@devtion/devcli 0.0.0-56ecf35 → 0.0.0-57a8ab9

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/.env CHANGED
@@ -23,6 +23,13 @@ FIREBASE_CF_URL_VERIFY_CONTRIBUTION=https://verifycontribution-mq4aqokliq-ew.a.r
23
23
 
24
24
  # The unique identifier for the Github client associated to the OAuth Application.
25
25
  AUTH_GITHUB_CLIENT_ID=e9f8a5fabdfe0d95618c
26
+ ### BANDADA AUTHENTICATION ###
27
+ # The Bandada API URL to be used for authentication.
28
+ BANDADA_API_URL=https://api.bandada.pse.dev/
29
+ # The Bandada group id that will be used for the credentials criteria (e.g. GH followers)
30
+ BANDADA_GROUP_ID=40887196405294111455930028907236
31
+ # The Bandada dashboard URL to administrate the groups
32
+ BANDADA_DASHBOARD_URL=https://bandada.pse.dev/
26
33
 
27
34
  ### AWS S3 STORAGE ###
28
35
  ### These configs are related to the configuration of the interaction with the
package/dist/index.js CHANGED
@@ -10,19 +10,19 @@
10
10
  */
11
11
  import { createCommand } from 'commander';
12
12
  import fs, { readFileSync, createWriteStream, existsSync, renameSync } from 'fs';
13
- import { dirname } from 'path';
13
+ import path, { dirname } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
- import { zKey } from 'snarkjs';
15
+ import { zKey, groth16 } from 'snarkjs';
16
16
  import boxen from 'boxen';
17
17
  import { pipeline } from 'node:stream';
18
18
  import { promisify } from 'node:util';
19
19
  import fetch$1 from 'node-fetch';
20
- import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract } from '@devtion/actions';
20
+ import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract, getAllCeremonies } from '@devtion/actions';
21
21
  import fetch from '@adobe/node-fetch-retry';
22
22
  import { request } from '@octokit/request';
23
23
  import { SingleBar, Presets } from 'cli-progress';
24
24
  import dotenv from 'dotenv';
25
- import { GithubAuthProvider, getAuth, signOut } from 'firebase/auth';
25
+ import { GithubAuthProvider, signInWithCustomToken, getAuth, signOut } from 'firebase/auth';
26
26
  import { getDiskInfoSync } from 'node-disk-info';
27
27
  import ora from 'ora';
28
28
  import { Timer } from 'timer-node';
@@ -36,7 +36,10 @@ import figlet from 'figlet';
36
36
  import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
37
37
  import clipboard from 'clipboardy';
38
38
  import open from 'open';
39
- import { Timestamp, onSnapshot } from 'firebase/firestore';
39
+ import { Identity } from '@semaphore-protocol/identity';
40
+ import { httpsCallable } from 'firebase/functions';
41
+ import { ApiSdk } from '@bandada/api-sdk';
42
+ import { Timestamp, onSnapshot, doc, collection, getDocs } from 'firebase/firestore';
40
43
  import readline from 'readline';
41
44
 
42
45
  /**
@@ -250,6 +253,10 @@ const config = new Conf({
250
253
  accessToken: {
251
254
  type: "string",
252
255
  default: ""
256
+ },
257
+ bandadaIdentity: {
258
+ type: "string",
259
+ default: ""
253
260
  }
254
261
  }
255
262
  });
@@ -310,6 +317,25 @@ const setLocalAccessToken = (token) => config.set("accessToken", token);
310
317
  * Delete the stored access token.
311
318
  */
312
319
  const deleteLocalAccessToken = () => config.delete("accessToken");
320
+ /**
321
+ * Return the Bandada identity, if present.
322
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
323
+ */
324
+ const getLocalBandadaIdentity = () => config.get("bandadaIdentity");
325
+ /**
326
+ * Check if the Bandada identity exists in the local storage.
327
+ * @returns <boolean>
328
+ */
329
+ const checkLocalBandadaIdentity = () => config.has("bandadaIdentity") && !!config.get("bandadaIdentity");
330
+ /**
331
+ * Set the Bandada identity.
332
+ * @param identity <string> - the Bandada identity to be stored.
333
+ */
334
+ const setLocalBandadaIdentity = (identity) => config.set("bandadaIdentity", identity);
335
+ /**
336
+ * Delete the stored Bandada identity.
337
+ */
338
+ const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity");
313
339
  /**
314
340
  * Get the complete local file path.
315
341
  * @param cwd <string> - the current working directory path.
@@ -420,7 +446,7 @@ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
420
446
  headers: {
421
447
  authorization: `token ${githubToken}`
422
448
  },
423
- per_page: params.perPage,
449
+ per_page: params.perPage, // max items per page = 100.
424
450
  page: params.page
425
451
  });
426
452
  if (response && response.status === 200)
@@ -468,8 +494,9 @@ const getPublicAttestationGist = async (githubToken, publicAttestationFilename)
468
494
  * @returns <string> - the third-party provider handle of the user.
469
495
  */
470
496
  const getUserHandleFromProviderUserId = (providerUserId) => {
471
- if (providerUserId.indexOf("-") === -1)
472
- showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
497
+ if (providerUserId.indexOf("-") === -1) {
498
+ return providerUserId;
499
+ }
473
500
  return providerUserId.split("-")[0];
474
501
  };
475
502
  /**
@@ -1560,16 +1587,27 @@ const checkAuth = async (firebaseApp) => {
1560
1587
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1561
1588
  // Retrieve local access token.
1562
1589
  const token = String(getLocalAccessToken());
1563
- // Get credentials.
1564
- const credentials = exchangeGithubTokenForCredentials(token);
1565
- // Sign in to Firebase using credentials.
1566
- await signInToFirebase(firebaseApp, credentials);
1590
+ let providerUserId;
1591
+ let username;
1592
+ const isLocalBandadaIdentityStored = checkLocalBandadaIdentity();
1593
+ if (isLocalBandadaIdentityStored) {
1594
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1595
+ providerUserId = userCredentials.user.uid;
1596
+ username = providerUserId;
1597
+ }
1598
+ else {
1599
+ // Get credentials.
1600
+ const credentials = exchangeGithubTokenForCredentials(token);
1601
+ // Sign in to Firebase using credentials.
1602
+ await signInToFirebase(firebaseApp, credentials);
1603
+ // Get Github unique identifier (handle-id).
1604
+ providerUserId = await getGithubProviderUserId(String(token));
1605
+ username = getUserHandleFromProviderUserId(providerUserId);
1606
+ }
1567
1607
  // Get current authenticated user.
1568
1608
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1569
- // Get Github unique identifier (handle-id).
1570
- const providerUserId = await getGithubProviderUserId(String(token));
1571
1609
  // Greet the user.
1572
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1610
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1573
1611
  return {
1574
1612
  user,
1575
1613
  token,
@@ -2225,6 +2263,91 @@ const auth = async () => {
2225
2263
  terminate(providerUserId);
2226
2264
  };
2227
2265
 
2266
+ const { BANDADA_API_URL } = process.env;
2267
+ const bandadaApi = new ApiSdk(BANDADA_API_URL);
2268
+ const addMemberToGroup = async (groupId, dashboardUrl, identity) => {
2269
+ const commitment = identity.commitment.toString();
2270
+ const group = await bandadaApi.getGroup(groupId);
2271
+ const providerName = group.credentials.id.split("_")[0].toLowerCase();
2272
+ // 6. open a new window with the url:
2273
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`;
2274
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`);
2275
+ open(url);
2276
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?");
2277
+ if (!confirmation)
2278
+ showError("You must join the Bandada group to continue the login process", true);
2279
+ };
2280
+ const isGroupMember = async (groupId, identity) => {
2281
+ const commitment = identity.commitment.toString();
2282
+ const isMember = await bandadaApi.isGroupMember(groupId, commitment);
2283
+ return isMember;
2284
+ };
2285
+
2286
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2287
+ const authBandada = async () => {
2288
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2289
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2290
+ spinner.start();
2291
+ // 1. check if _identity string exists in local storage
2292
+ let identityString;
2293
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2294
+ if (isIdentityStringStored) {
2295
+ identityString = getLocalBandadaIdentity();
2296
+ spinner.succeed(`Identity seed found\n`);
2297
+ }
2298
+ else {
2299
+ spinner.warn(`Identity seed not found\n`);
2300
+ // 2. generate a random _identity string and save it in local storage
2301
+ const { seed } = await prompts({
2302
+ type: "text",
2303
+ name: "seed",
2304
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2305
+ initial: false
2306
+ });
2307
+ identityString = seed;
2308
+ setLocalBandadaIdentity(identityString);
2309
+ }
2310
+ // 3. create a semaphore identity with _identity string as a seed
2311
+ const identity = new Identity(identityString);
2312
+ // 4. check if the user is a member of the group
2313
+ console.log(`Checking Bandada membership...`);
2314
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2315
+ if (!isMember) {
2316
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2317
+ }
2318
+ // 5. generate a proof that the user owns the commitment.
2319
+ spinner.text = `Generating proof of identity...`;
2320
+ spinner.start();
2321
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2322
+ const { proof, publicSignals } = await groth16.fullProve({
2323
+ identityTrapdoor: identity.trapdoor,
2324
+ identityNullifier: identity.nullifier,
2325
+ externalNullifier: BANDADA_GROUP_ID
2326
+ }, path.join(path.resolve(), "/public/mini-semaphore.wasm"), path.join(path.resolve(), "/public/mini-semaphore.zkey"));
2327
+ spinner.succeed(`Proof generated.\n`);
2328
+ spinner.text = `Sending proof to verification...`;
2329
+ spinner.start();
2330
+ // 6. send proof to a cloud function that verifies it and checks membership
2331
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2332
+ const result = await cf({
2333
+ proof,
2334
+ publicSignals
2335
+ });
2336
+ const { valid, token, message } = result.data;
2337
+ if (!valid) {
2338
+ showError(message, true);
2339
+ }
2340
+ spinner.succeed(`Proof verified.\n`);
2341
+ spinner.text = `Authenticating...`;
2342
+ spinner.start();
2343
+ // 7. Auth to p0tion firebase
2344
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
2345
+ setLocalAccessToken(token);
2346
+ spinner.succeed(`Authenticated as ${theme.text.bold(userCredentials.user.uid)}.`);
2347
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2348
+ process.exit(0);
2349
+ };
2350
+
2228
2351
  /**
2229
2352
  * Return the verification result for latest contribution.
2230
2353
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2416,8 +2539,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2416
2539
  // Write public attestation locally.
2417
2540
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2418
2541
  await sleep(1000); // workaround for file descriptor unexpected close.
2419
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2420
- console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2542
+ let gistUrl = "";
2543
+ const isBandada = checkLocalBandadaIdentity();
2544
+ if (!isBandada) {
2545
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2546
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2547
+ }
2421
2548
  // Prepare a ready-to-share tweet.
2422
2549
  await handleTweetGeneration(ceremonyName, gistUrl);
2423
2550
  };
@@ -3133,6 +3260,7 @@ const logout = async () => {
3133
3260
  await signOut(auth);
3134
3261
  // Delete local token.
3135
3262
  deleteLocalAccessToken();
3263
+ deleteLocalBandadaIdentity();
3136
3264
  await sleep(3000); // ~3s.
3137
3265
  spinner.stop();
3138
3266
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3194,6 +3322,37 @@ const listCeremonies = async () => {
3194
3322
  }
3195
3323
  };
3196
3324
 
3325
+ const listParticipants = async () => {
3326
+ try {
3327
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3328
+ const allCeremonies = await getAllCeremonies(firestoreDatabase);
3329
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true);
3330
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id);
3331
+ const participantsRef = collection(docRef, "participants");
3332
+ const participantsSnapshot = await getDocs(participantsRef);
3333
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data().userId);
3334
+ console.log(participants);
3335
+ /* const usersRef = collection(firestoreDatabase, "users")
3336
+ const usersSnapshot = await getDocs(usersRef)
3337
+ const users = usersSnapshot.docs.map((userDoc) => userDoc.data())
3338
+ console.log(users) */
3339
+ }
3340
+ catch (err) {
3341
+ showError(`Something went wrong: ${err.toString()}`, true);
3342
+ }
3343
+ process.exit(0);
3344
+ };
3345
+
3346
+ const setCeremonyCommands = (program) => {
3347
+ const ceremony = program.command("ceremony").description("manage ceremonies");
3348
+ ceremony
3349
+ .command("participants")
3350
+ .description("retrieve participants list of a ceremony")
3351
+ .requiredOption("-c, --ceremony <string>", "the prefix of the ceremony you want to retrieve information about", "")
3352
+ .action(listParticipants);
3353
+ return ceremony;
3354
+ };
3355
+
3197
3356
  // Get pkg info (e.g., name, version).
3198
3357
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3199
3358
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3202,6 +3361,10 @@ const program = createCommand();
3202
3361
  program.name(name).description(description).version(version);
3203
3362
  // User commands.
3204
3363
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3364
+ program
3365
+ .command("auth-bandada")
3366
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
3367
+ .action(authBandada);
3205
3368
  program
3206
3369
  .command("contribute")
3207
3370
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -3220,25 +3383,26 @@ program
3220
3383
  .action(logout);
3221
3384
  program
3222
3385
  .command("validate")
3223
- .description("Validate that a Ceremony Setup file is correct")
3386
+ .description("validate that a Ceremony Setup file is correct")
3224
3387
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3225
3388
  .option("-c, --constraints <number>", "The number of constraints to check against")
3226
3389
  .action(validate);
3227
3390
  // Only coordinator commands.
3228
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3229
- ceremony
3391
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3392
+ coordinate
3230
3393
  .command("setup")
3231
3394
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3232
3395
  .option("-t, --template <path>", "The path to the ceremony setup template", "")
3233
3396
  .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3234
3397
  .action(setup);
3235
- ceremony
3398
+ coordinate
3236
3399
  .command("observe")
3237
3400
  .description("observe in real-time the waiting queue of each ceremony circuit")
3238
3401
  .action(observe);
3239
- ceremony
3402
+ coordinate
3240
3403
  .command("finalize")
3241
3404
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3242
3405
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3243
3406
  .action(finalize);
3407
+ setCeremonyCommands(program);
3244
3408
  program.parseAsync(process.argv);
@@ -0,0 +1,2 @@
1
+ declare const authBandada: () => Promise<never>;
2
+ export default authBandada;
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ declare const setCeremonyCommands: (program: Command) => Command;
3
+ export default setCeremonyCommands;
@@ -0,0 +1,2 @@
1
+ declare const listParticipants: () => Promise<never>;
2
+ export default listParticipants;
@@ -1,5 +1,6 @@
1
1
  export { default as setup } from "./setup.js";
2
2
  export { default as auth } from "./auth.js";
3
+ export { default as authBandada } from "./authBandada.js";
3
4
  export { default as contribute } from "./contribute.js";
4
5
  export { default as observe } from "./observe.js";
5
6
  export { default as finalize } from "./finalize.js";
@@ -0,0 +1,6 @@
1
+ import { GroupResponse } from "@bandada/api-sdk";
2
+ import { Identity } from "@semaphore-protocol/identity";
3
+ export declare const getGroup: (groupId: string) => Promise<GroupResponse | null>;
4
+ export declare const getMembersOfGroup: (groupId: string) => Promise<string[] | null>;
5
+ export declare const addMemberToGroup: (groupId: string, dashboardUrl: string, identity: Identity) => Promise<void>;
6
+ export declare const isGroupMember: (groupId: string, identity: Identity) => Promise<boolean>;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { Dirent, Stats } from "fs";
3
4
  /**
4
5
  * Check a directory path.
@@ -35,6 +35,25 @@ export declare const setLocalAccessToken: (token: string) => void;
35
35
  * Delete the stored access token.
36
36
  */
37
37
  export declare const deleteLocalAccessToken: () => void;
38
+ /**
39
+ * Return the Bandada identity, if present.
40
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
41
+ */
42
+ export declare const getLocalBandadaIdentity: () => string | unknown;
43
+ /**
44
+ * Check if the Bandada identity exists in the local storage.
45
+ * @returns <boolean>
46
+ */
47
+ export declare const checkLocalBandadaIdentity: () => boolean;
48
+ /**
49
+ * Set the Bandada identity.
50
+ * @param identity <string> - the Bandada identity to be stored.
51
+ */
52
+ export declare const setLocalBandadaIdentity: (identity: string) => void;
53
+ /**
54
+ * Delete the stored Bandada identity.
55
+ */
56
+ export declare const deleteLocalBandadaIdentity: () => void;
38
57
  /**
39
58
  * Get the complete local file path.
40
59
  * @param cwd <string> - the current working directory path.
@@ -63,3 +63,15 @@ export type GithubGistFile = {
63
63
  raw_url: string;
64
64
  size: number;
65
65
  };
66
+ /**
67
+ * Define the return object of the function that verifies the Bandada membership and proof.
68
+ * @typedef {Object} VerifiedBandadaResponse
69
+ * @property {boolean} valid - true if the proof is valid and the user is a member of the group; otherwise false.
70
+ * @property {string} message - a message describing the result of the verification.
71
+ * @property {string} token - the custom access token.
72
+ */
73
+ export type VerifiedBandadaResponse = {
74
+ valid: boolean;
75
+ message: string;
76
+ token: string;
77
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devtion/devcli",
3
3
  "type": "module",
4
- "version": "0.0.0-56ecf35",
4
+ "version": "0.0.0-57a8ab9",
5
5
  "description": "All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies",
6
6
  "repository": "git@github.com:privacy-scaling-explorations/p0tion.git",
7
7
  "homepage": "https://github.com/privacy-scaling-explorations/p0tion",
@@ -34,11 +34,13 @@
34
34
  "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript",
35
35
  "start": "ts-node --esm ./src/index.ts",
36
36
  "auth": "yarn start auth",
37
+ "auth:bandada": "yarn start auth-bandada",
37
38
  "contribute": "yarn start contribute",
38
39
  "clean": "yarn start clean",
39
40
  "list": "yarn start list",
40
41
  "logout": "yarn start logout",
41
42
  "validate": "yarn start validate",
43
+ "ceremony:participants": "yarn start ceremony participants",
42
44
  "coordinate:setup": "yarn start coordinate setup",
43
45
  "coordinate:observe": "yarn start coordinate observe",
44
46
  "coordinate:finalize": "yarn start coordinate finalize",
@@ -65,10 +67,12 @@
65
67
  "dependencies": {
66
68
  "@adobe/node-fetch-retry": "^2.2.0",
67
69
  "@aws-sdk/client-s3": "^3.329.0",
70
+ "@bandada/api-sdk": "^1.0.0-beta.1",
68
71
  "@devtion/actions": "latest",
69
72
  "@octokit/auth-oauth-app": "^5.0.5",
70
73
  "@octokit/auth-oauth-device": "^4.0.4",
71
74
  "@octokit/request": "^6.2.3",
75
+ "@semaphore-protocol/identity": "^3.15.1",
72
76
  "blakejs": "^1.2.1",
73
77
  "boxen": "^7.1.0",
74
78
  "chalk": "^5.2.0",
@@ -78,6 +82,7 @@
78
82
  "commander": "^10.0.1",
79
83
  "conf": "^11.0.1",
80
84
  "dotenv": "^16.0.3",
85
+ "ethers": "^6.9.0",
81
86
  "figlet": "^1.6.0",
82
87
  "firebase": "^9.21.0",
83
88
  "log-symbols": "^5.1.0",
@@ -90,12 +95,12 @@
90
95
  "prompts": "^2.4.2",
91
96
  "rimraf": "^5.0.0",
92
97
  "rollup": "^3.21.6",
93
- "snarkjs": "^0.6.11",
98
+ "snarkjs": "0.7.3",
94
99
  "timer-node": "^5.0.7",
95
100
  "winston": "^3.8.2"
96
101
  },
97
102
  "publishConfig": {
98
103
  "access": "public"
99
104
  },
100
- "gitHead": "7d72963a1027f3e86cecf40d3255f04cc74e4826"
105
+ "gitHead": "6bddf60f1121786c19ad4437e0448de4c859b829"
101
106
  }
@@ -0,0 +1,99 @@
1
+ import { Identity } from "@semaphore-protocol/identity"
2
+
3
+ import { commonTerms } from "@devtion/actions"
4
+ import { httpsCallable } from "firebase/functions"
5
+ import { groth16 } from "snarkjs"
6
+ import path from "path"
7
+ import { getAuth, signInWithCustomToken } from "firebase/auth"
8
+ import theme from "../lib/theme.js"
9
+ import { customSpinner } from "../lib/utils.js"
10
+ import { VerifiedBandadaResponse } from "../types/index.js"
11
+ import { showError } from "../lib/errors.js"
12
+ import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
13
+ import { addMemberToGroup, isGroupMember } from "../lib/bandada.js"
14
+ import {
15
+ checkLocalBandadaIdentity,
16
+ getLocalBandadaIdentity,
17
+ setLocalAccessToken,
18
+ setLocalBandadaIdentity
19
+ } from "../lib/localConfigs.js"
20
+ import prompts from "prompts"
21
+
22
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env
23
+
24
+ const authBandada = async () => {
25
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices()
26
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`)
27
+ spinner.start()
28
+ // 1. check if _identity string exists in local storage
29
+ let identityString: string | unknown
30
+ const isIdentityStringStored = checkLocalBandadaIdentity()
31
+ if (isIdentityStringStored) {
32
+ identityString = getLocalBandadaIdentity()
33
+ spinner.succeed(`Identity seed found\n`)
34
+ } else {
35
+ spinner.warn(`Identity seed not found\n`)
36
+ // 2. generate a random _identity string and save it in local storage
37
+ const { seed } = await prompts({
38
+ type: "text",
39
+ name: "seed",
40
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
41
+ initial: false
42
+ })
43
+ identityString = seed as string
44
+ setLocalBandadaIdentity(identityString as string)
45
+ }
46
+ // 3. create a semaphore identity with _identity string as a seed
47
+ const identity = new Identity(identityString as string)
48
+
49
+ // 4. check if the user is a member of the group
50
+ console.log(`Checking Bandada membership...`)
51
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity)
52
+ if (!isMember) {
53
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity)
54
+ }
55
+
56
+ // 5. generate a proof that the user owns the commitment.
57
+ spinner.text = `Generating proof of identity...`
58
+ spinner.start()
59
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
60
+ const { proof, publicSignals } = await groth16.fullProve(
61
+ {
62
+ identityTrapdoor: identity.trapdoor,
63
+ identityNullifier: identity.nullifier,
64
+ externalNullifier: BANDADA_GROUP_ID
65
+ },
66
+ path.join(path.resolve(), "/public/mini-semaphore.wasm"),
67
+ path.join(path.resolve(), "/public/mini-semaphore.zkey")
68
+ )
69
+ spinner.succeed(`Proof generated.\n`)
70
+ spinner.text = `Sending proof to verification...`
71
+ spinner.start()
72
+ // 6. send proof to a cloud function that verifies it and checks membership
73
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof)
74
+ const result = await cf({
75
+ proof,
76
+ publicSignals
77
+ })
78
+ const { valid, token, message } = result.data as VerifiedBandadaResponse
79
+ if (!valid) {
80
+ showError(message, true)
81
+ }
82
+ spinner.succeed(`Proof verified.\n`)
83
+ spinner.text = `Authenticating...`
84
+ spinner.start()
85
+ // 7. Auth to p0tion firebase
86
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
87
+ setLocalAccessToken(token)
88
+ spinner.succeed(`Authenticated as ${theme.text.bold(userCredentials.user.uid)}.`)
89
+
90
+ console.log(
91
+ `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
92
+ `phase2cli logout`
93
+ )} command`
94
+ )
95
+
96
+ process.exit(0)
97
+ }
98
+
99
+ export default authBandada
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander"
2
+ import listParticipants from "./listParticipants.js"
3
+
4
+ const setCeremonyCommands = (program: Command) => {
5
+ const ceremony = program.command("ceremony").description("manage ceremonies")
6
+
7
+ ceremony
8
+ .command("participants")
9
+ .description("retrieve participants list of a ceremony")
10
+ .requiredOption(
11
+ "-c, --ceremony <string>",
12
+ "the prefix of the ceremony you want to retrieve information about",
13
+ ""
14
+ )
15
+ .action(listParticipants)
16
+
17
+ return ceremony
18
+ }
19
+
20
+ export default setCeremonyCommands
@@ -0,0 +1,30 @@
1
+ import { collection, doc, getDocs } from "firebase/firestore"
2
+ import { commonTerms, getAllCeremonies } from "@devtion/actions"
3
+ import { bootstrapCommandExecutionAndServices } from "../../lib/services.js"
4
+ import { showError } from "../../lib/errors.js"
5
+ import { promptForCeremonySelection } from "../../lib/prompts.js"
6
+
7
+ const listParticipants = async () => {
8
+ try {
9
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices()
10
+
11
+ const allCeremonies = await getAllCeremonies(firestoreDatabase)
12
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true)
13
+
14
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id)
15
+ const participantsRef = collection(docRef, "participants")
16
+ const participantsSnapshot = await getDocs(participantsRef)
17
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data().userId)
18
+ console.log(participants)
19
+
20
+ /* const usersRef = collection(firestoreDatabase, "users")
21
+ const usersSnapshot = await getDocs(usersRef)
22
+ const users = usersSnapshot.docs.map((userDoc) => userDoc.data())
23
+ console.log(users) */
24
+ } catch (err: any) {
25
+ showError(`Something went wrong: ${err.toString()}`, true)
26
+ }
27
+ process.exit(0)
28
+ }
29
+
30
+ export default listParticipants
@@ -41,7 +41,7 @@ import {
41
41
  } from "../lib/utils.js"
42
42
  import { COMMAND_ERRORS, showError } from "../lib/errors.js"
43
43
  import { authWithToken, bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
44
- import { getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
44
+ import { checkLocalBandadaIdentity, getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
45
45
  import theme from "../lib/theme.js"
46
46
  import { checkAndMakeNewDirectoryIfNonexistent, writeFile } from "../lib/files.js"
47
47
 
@@ -419,14 +419,19 @@ export const handlePublicAttestation = async (
419
419
 
420
420
  await sleep(1000) // workaround for file descriptor unexpected close.
421
421
 
422
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
423
-
424
- console.log(
425
- `\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
426
- theme.text.underlined(gistUrl)
427
- )})`
428
- )
422
+ let gistUrl = ""
423
+ const isBandada = checkLocalBandadaIdentity()
424
+ if (!isBandada) {
425
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
429
426
 
427
+ console.log(
428
+ `\n${
429
+ theme.symbols.info
430
+ } Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
431
+ theme.text.underlined(gistUrl)
432
+ )})`
433
+ )
434
+ }
430
435
  // Prepare a ready-to-share tweet.
431
436
  await handleTweetGeneration(ceremonyName, gistUrl)
432
437
  }
@@ -1,5 +1,6 @@
1
1
  export { default as setup } from "./setup.js"
2
2
  export { default as auth } from "./auth.js"
3
+ export { default as authBandada } from "./authBandada.js"
3
4
  export { default as contribute } from "./contribute.js"
4
5
  export { default as observe } from "./observe.js"
5
6
  export { default as finalize } from "./finalize.js"
@@ -6,7 +6,7 @@ import { showError } from "../lib/errors.js"
6
6
  import { askForConfirmation } from "../lib/prompts.js"
7
7
  import { customSpinner, sleep, terminate } from "../lib/utils.js"
8
8
  import theme from "../lib/theme.js"
9
- import { deleteLocalAccessToken } from "../lib/localConfigs.js"
9
+ import { deleteLocalAccessToken, deleteLocalBandadaIdentity } from "../lib/localConfigs.js"
10
10
 
11
11
  /**
12
12
  * Logout command.
@@ -53,6 +53,7 @@ const logout = async () => {
53
53
 
54
54
  // Delete local token.
55
55
  deleteLocalAccessToken()
56
+ deleteLocalBandadaIdentity()
56
57
 
57
58
  await sleep(3000) // ~3s.
58
59
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { fileURLToPath } from "url"
7
7
  import {
8
8
  setup,
9
9
  auth,
10
+ authBandada,
10
11
  contribute,
11
12
  observe,
12
13
  finalize,
@@ -15,6 +16,7 @@ import {
15
16
  validate,
16
17
  listCeremonies
17
18
  } from "./commands/index.js"
19
+ import setCeremonyCommands from "./commands/ceremony/index.js"
18
20
 
19
21
  // Get pkg info (e.g., name, version).
20
22
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`
@@ -26,6 +28,10 @@ program.name(name).description(description).version(version)
26
28
 
27
29
  // User commands.
28
30
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth)
31
+ program
32
+ .command("auth-bandada")
33
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
34
+ .action(authBandada)
29
35
  program
30
36
  .command("contribute")
31
37
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -44,27 +50,27 @@ program
44
50
  .action(logout)
45
51
  program
46
52
  .command("validate")
47
- .description("Validate that a Ceremony Setup file is correct")
53
+ .description("validate that a Ceremony Setup file is correct")
48
54
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
49
55
  .option("-c, --constraints <number>", "The number of constraints to check against")
50
56
  .action(validate)
51
57
 
52
58
  // Only coordinator commands.
53
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony")
59
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony")
54
60
 
55
- ceremony
61
+ coordinate
56
62
  .command("setup")
57
63
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
58
64
  .option("-t, --template <path>", "The path to the ceremony setup template", "")
59
65
  .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
60
66
  .action(setup)
61
67
 
62
- ceremony
68
+ coordinate
63
69
  .command("observe")
64
70
  .description("observe in real-time the waiting queue of each ceremony circuit")
65
71
  .action(observe)
66
72
 
67
- ceremony
73
+ coordinate
68
74
  .command("finalize")
69
75
  .description(
70
76
  "finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract"
@@ -72,4 +78,6 @@ ceremony
72
78
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
73
79
  .action(finalize)
74
80
 
81
+ setCeremonyCommands(program)
82
+
75
83
  program.parseAsync(process.argv)
@@ -0,0 +1,51 @@
1
+ import { ApiSdk, GroupResponse } from "@bandada/api-sdk"
2
+ import { Identity } from "@semaphore-protocol/identity"
3
+ import open from "open"
4
+
5
+ import { askForConfirmation } from "../lib/prompts.js"
6
+ import { showError } from "./errors.js"
7
+ import theme from "../lib/theme.js"
8
+
9
+ const { BANDADA_API_URL } = process.env
10
+
11
+ const bandadaApi = new ApiSdk(BANDADA_API_URL)
12
+
13
+ export const getGroup = async (groupId: string): Promise<GroupResponse | null> => {
14
+ try {
15
+ const group = await bandadaApi.getGroup(groupId)
16
+ return group
17
+ } catch (error: any) {
18
+ showError(`Bandada getGroup error: ${error}`, true)
19
+ return null
20
+ }
21
+ }
22
+
23
+ export const getMembersOfGroup = async (groupId: string): Promise<string[] | null> => {
24
+ try {
25
+ const group = await bandadaApi.getGroup(groupId)
26
+ return group.members
27
+ } catch (error: any) {
28
+ showError(`Bandada getMembersOfGroup error: ${error}`, true)
29
+ return null
30
+ }
31
+ }
32
+
33
+ export const addMemberToGroup = async (groupId: string, dashboardUrl: string, identity: Identity) => {
34
+ const commitment = identity.commitment.toString()
35
+ const group = await bandadaApi.getGroup(groupId)
36
+ const providerName = group.credentials.id.split("_")[0].toLowerCase()
37
+
38
+ // 6. open a new window with the url:
39
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`
40
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`)
41
+ open(url)
42
+
43
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?")
44
+ if (!confirmation) showError("You must join the Bandada group to continue the login process", true)
45
+ }
46
+
47
+ export const isGroupMember = async (groupId: string, identity: Identity): Promise<boolean> => {
48
+ const commitment = identity.commitment.toString()
49
+ const isMember: boolean = await bandadaApi.isGroupMember(groupId, commitment)
50
+ return isMember
51
+ }
@@ -24,6 +24,10 @@ const config = new Conf({
24
24
  accessToken: {
25
25
  type: "string",
26
26
  default: ""
27
+ },
28
+ bandadaIdentity: {
29
+ type: "string",
30
+ default: ""
27
31
  }
28
32
  }
29
33
  })
@@ -91,6 +95,29 @@ export const setLocalAccessToken = (token: string) => config.set("accessToken",
91
95
  */
92
96
  export const deleteLocalAccessToken = () => config.delete("accessToken")
93
97
 
98
+ /**
99
+ * Return the Bandada identity, if present.
100
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
101
+ */
102
+ export const getLocalBandadaIdentity = (): string | unknown => config.get("bandadaIdentity")
103
+
104
+ /**
105
+ * Check if the Bandada identity exists in the local storage.
106
+ * @returns <boolean>
107
+ */
108
+ export const checkLocalBandadaIdentity = (): boolean => config.has("bandadaIdentity") && !!config.get("bandadaIdentity")
109
+
110
+ /**
111
+ * Set the Bandada identity.
112
+ * @param identity <string> - the Bandada identity to be stored.
113
+ */
114
+ export const setLocalBandadaIdentity = (identity: string) => config.set("bandadaIdentity", identity)
115
+
116
+ /**
117
+ * Delete the stored Bandada identity.
118
+ */
119
+ export const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity")
120
+
94
121
  /**
95
122
  * Get the complete local file path.
96
123
  * @param cwd <string> - the current working directory path.
@@ -6,13 +6,18 @@ import {
6
6
  import clear from "clear"
7
7
  import figlet from "figlet"
8
8
  import { FirebaseApp } from "firebase/app"
9
- import { OAuthCredential } from "firebase/auth"
9
+ import { OAuthCredential, getAuth, signInWithCustomToken } from "firebase/auth"
10
10
  import dotenv from "dotenv"
11
11
  import { fileURLToPath } from "url"
12
12
  import { dirname } from "path"
13
13
  import { AuthUser } from "../types/index.js"
14
14
  import { CONFIG_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
15
- import { checkLocalAccessToken, deleteLocalAccessToken, getLocalAccessToken } from "./localConfigs.js"
15
+ import {
16
+ checkLocalAccessToken,
17
+ checkLocalBandadaIdentity,
18
+ deleteLocalAccessToken,
19
+ getLocalAccessToken
20
+ } from "./localConfigs.js"
16
21
  import theme from "./theme.js"
17
22
  import { exchangeGithubTokenForCredentials, getGithubProviderUserId, getUserHandleFromProviderUserId } from "./utils.js"
18
23
 
@@ -164,22 +169,30 @@ export const checkAuth = async (firebaseApp: FirebaseApp): Promise<AuthUser> =>
164
169
  // Retrieve local access token.
165
170
  const token = String(getLocalAccessToken())
166
171
 
167
- // Get credentials.
168
- const credentials = exchangeGithubTokenForCredentials(token)
169
-
170
- // Sign in to Firebase using credentials.
171
- await signInToFirebase(firebaseApp, credentials)
172
+ let providerUserId: string
173
+ let username: string
174
+ const isLocalBandadaIdentityStored = checkLocalBandadaIdentity()
175
+ if (isLocalBandadaIdentityStored) {
176
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
177
+ providerUserId = userCredentials.user.uid
178
+ username = providerUserId
179
+ } else {
180
+ // Get credentials.
181
+ const credentials = exchangeGithubTokenForCredentials(token)
182
+
183
+ // Sign in to Firebase using credentials.
184
+ await signInToFirebase(firebaseApp, credentials)
185
+
186
+ // Get Github unique identifier (handle-id).
187
+ providerUserId = await getGithubProviderUserId(String(token))
188
+ username = getUserHandleFromProviderUserId(providerUserId)
189
+ }
172
190
 
173
191
  // Get current authenticated user.
174
192
  const user = getCurrentFirebaseAuthUser(firebaseApp)
175
193
 
176
- // Get Github unique identifier (handle-id).
177
- const providerUserId = await getGithubProviderUserId(String(token))
178
-
179
194
  // Greet the user.
180
- console.log(
181
- `Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`
182
- )
195
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`)
183
196
 
184
197
  return {
185
198
  user,
package/src/lib/utils.ts CHANGED
@@ -155,7 +155,9 @@ export const getPublicAttestationGist = async (
155
155
  * @returns <string> - the third-party provider handle of the user.
156
156
  */
157
157
  export const getUserHandleFromProviderUserId = (providerUserId: string): string => {
158
- if (providerUserId.indexOf("-") === -1) showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
158
+ if (providerUserId.indexOf("-") === -1) {
159
+ return providerUserId
160
+ }
159
161
 
160
162
  return providerUserId.split("-")[0]
161
163
  }
@@ -68,3 +68,16 @@ export type GithubGistFile = {
68
68
  raw_url: string
69
69
  size: number
70
70
  }
71
+
72
+ /**
73
+ * Define the return object of the function that verifies the Bandada membership and proof.
74
+ * @typedef {Object} VerifiedBandadaResponse
75
+ * @property {boolean} valid - true if the proof is valid and the user is a member of the group; otherwise false.
76
+ * @property {string} message - a message describing the result of the verification.
77
+ * @property {string} token - the custom access token.
78
+ */
79
+ export type VerifiedBandadaResponse = {
80
+ valid: boolean
81
+ message: string
82
+ token: string
83
+ }