@devtion/devcli 0.0.0-57a8ab9 → 0.0.0-671e653

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
@@ -45,4 +45,11 @@ CONFIG_CEREMONY_BUCKET_POSTFIX=-p0tion-development-environment
45
45
  # The amount of time in seconds which indicates the duration about the validity of a pre-signed URL.
46
46
  # default: 7200 seconds = 2 hours.
47
47
  CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS=7200
48
+
49
+
50
+ # Sign In With Ethereum
51
+ # Auth0 client id
52
+ AUTH_SIWE_CLIENT_ID=tRuFnJNoPTJtKr1RynYfty6uJ16QzHXA
53
+ # The Auth0 application url that support SIWE + Device Flow Authentication
54
+ AUTH0_APPLICATION_URL=https://dev-l0tyk1agsmopw1xa.us.auth0.com
48
55
 
package/dist/index.js CHANGED
@@ -238,6 +238,14 @@ const checkAndMakeNewDirectoryIfNonexistent = (directoryLocalPath) => {
238
238
  const writeLocalJsonFile = (filePath, data) => {
239
239
  fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
240
240
  };
241
+ /**
242
+ * Return the local current project directory name.
243
+ * @returns <string> - the local project (e.g., dist/) directory name.
244
+ */
245
+ const getLocalDirname = () => {
246
+ const filename = fileURLToPath(import.meta.url);
247
+ return path.dirname(filename);
248
+ };
241
249
 
242
250
  // Get npm package name.
243
251
  const packagePath$4 = `${dirname(fileURLToPath(import.meta.url))}/..`;
@@ -257,6 +265,10 @@ const config = new Conf({
257
265
  bandadaIdentity: {
258
266
  type: "string",
259
267
  default: ""
268
+ },
269
+ authMethod: {
270
+ type: "string",
271
+ default: ""
260
272
  }
261
273
  }
262
274
  });
@@ -336,6 +348,20 @@ const setLocalBandadaIdentity = (identity) => config.set("bandadaIdentity", iden
336
348
  * Delete the stored Bandada identity.
337
349
  */
338
350
  const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity");
351
+ /**
352
+ * Return the authentication method, if present.
353
+ * @returns <string | undefined> - the authentication method if present, otherwise undefined.
354
+ */
355
+ const getLocalAuthMethod = () => config.get("authMethod");
356
+ /**
357
+ * Set the authentication method.
358
+ * @param method <string> - the authentication method to be stored.
359
+ */
360
+ const setLocalAuthMethod = (method) => config.set("authMethod", method);
361
+ /**
362
+ * Delete the stored authentication method.
363
+ */
364
+ const deleteLocalAuthMethod = () => config.delete("authMethod");
339
365
  /**
340
366
  * Get the complete local file path.
341
367
  * @param cwd <string> - the current working directory path.
@@ -1589,20 +1615,30 @@ const checkAuth = async (firebaseApp) => {
1589
1615
  const token = String(getLocalAccessToken());
1590
1616
  let providerUserId;
1591
1617
  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);
1618
+ const authMethod = getLocalAuthMethod();
1619
+ switch (authMethod) {
1620
+ case "github": {
1621
+ // Get credentials.
1622
+ const credentials = exchangeGithubTokenForCredentials(token);
1623
+ // Sign in to Firebase using credentials.
1624
+ await signInToFirebase(firebaseApp, credentials);
1625
+ // Get Github unique identifier (handle-id).
1626
+ providerUserId = await getGithubProviderUserId(String(token));
1627
+ username = getUserHandleFromProviderUserId(providerUserId);
1628
+ break;
1629
+ }
1630
+ case "bandada": {
1631
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1632
+ providerUserId = userCredentials.user.uid;
1633
+ username = providerUserId;
1634
+ break;
1635
+ }
1636
+ case "siwe": {
1637
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1638
+ providerUserId = userCredentials.user.uid;
1639
+ username = providerUserId;
1640
+ break;
1641
+ }
1606
1642
  }
1607
1643
  // Get current authenticated user.
1608
1644
  const user = getCurrentFirebaseAuthUser(firebaseApp);
@@ -2243,6 +2279,7 @@ const auth = async () => {
2243
2279
  // Generate a new access token using Github Device Flow (OAuth 2.0).
2244
2280
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2245
2281
  // Store the new access token.
2282
+ setLocalAuthMethod("github");
2246
2283
  setLocalAccessToken(newToken);
2247
2284
  }
2248
2285
  else
@@ -2285,67 +2322,220 @@ const isGroupMember = async (groupId, identity) => {
2285
2322
 
2286
2323
  const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2287
2324
  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
2325
+ try {
2326
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2327
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2328
+ spinner.start();
2329
+ // 1. check if _identity string exists in local storage
2330
+ let identityString;
2331
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2332
+ if (isIdentityStringStored) {
2333
+ identityString = getLocalBandadaIdentity();
2334
+ spinner.succeed(`Identity seed found\n`);
2335
+ }
2336
+ else {
2337
+ spinner.warn(`Identity seed not found\n`);
2338
+ // 2. generate a random _identity string and save it in local storage
2339
+ const { seed } = await prompts({
2340
+ type: "text",
2341
+ name: "seed",
2342
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2343
+ initial: false
2344
+ });
2345
+ identityString = seed;
2346
+ setLocalBandadaIdentity(identityString);
2347
+ }
2348
+ // 3. create a semaphore identity with _identity string as a seed
2349
+ const identity = new Identity(identityString);
2350
+ // 4. check if the user is a member of the group
2351
+ console.log(`Checking Bandada membership...`);
2352
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2353
+ if (!isMember) {
2354
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2355
+ }
2356
+ // 5. generate a proof that the user owns the commitment.
2357
+ spinner.text = `Generating proof of identity...`;
2358
+ spinner.start();
2359
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2360
+ const initDirectoryName = getLocalDirname();
2361
+ const directoryName = initDirectoryName.includes("/src") ? "." : initDirectoryName;
2362
+ const { proof, publicSignals } = await groth16.fullProve({
2363
+ identityTrapdoor: identity.trapdoor,
2364
+ identityNullifier: identity.nullifier,
2365
+ externalNullifier: BANDADA_GROUP_ID
2366
+ }, `${directoryName}/public/mini-semaphore.wasm`, `${directoryName}/public/mini-semaphore.zkey`);
2367
+ spinner.succeed(`Proof generated.\n`);
2368
+ spinner.text = `Sending proof to verification...`;
2369
+ spinner.start();
2370
+ // 6. send proof to a cloud function that verifies it and checks membership
2371
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2372
+ const result = await cf({
2373
+ proof,
2374
+ publicSignals
2306
2375
  });
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...`;
2376
+ const { valid, token, message } = result.data;
2377
+ if (!valid) {
2378
+ showError(message, true);
2379
+ deleteLocalAuthMethod();
2380
+ deleteLocalAccessToken();
2381
+ deleteLocalBandadaIdentity();
2382
+ }
2383
+ spinner.succeed(`Proof verified.\n`);
2384
+ spinner.text = `Authenticating...`;
2385
+ spinner.start();
2386
+ // 7. Auth to p0tion firebase
2387
+ const credentials = await signInWithCustomToken(getAuth(), token);
2388
+ setLocalAuthMethod("bandada");
2389
+ setLocalAccessToken(token);
2390
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2391
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2392
+ }
2393
+ catch (error) {
2394
+ // Delete local token.
2395
+ console.log("An error crashed the process. Deleting local token and identity.");
2396
+ console.error(error);
2397
+ deleteLocalAuthMethod();
2398
+ deleteLocalAccessToken();
2399
+ deleteLocalBandadaIdentity();
2400
+ }
2401
+ process.exit(0);
2402
+ };
2403
+
2404
+ const showVerificationCodeAndUri = async (OAuthDeviceCode) => {
2405
+ // Copy code to clipboard.
2406
+ let noClipboard = false;
2407
+ try {
2408
+ clipboard.writeSync(OAuthDeviceCode.user_code);
2409
+ clipboard.readSync();
2410
+ }
2411
+ catch (error) {
2412
+ noClipboard = true;
2413
+ }
2414
+ // Display data.
2415
+ console.log(`${theme.symbols.warning} Visit ${theme.text.bold(theme.text.underlined(OAuthDeviceCode.verification_uri))} on this device to generate a new token and authenticate\n`);
2416
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n");
2417
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``;
2418
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${theme.symbols.success}\n`);
2419
+ const spinner = customSpinner(`Redirecting to Github...`, `clock`);
2329
2420
  spinner.start();
2330
- // 6. send proof to a cloud function that verifies it and checks membership
2331
- const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2421
+ await sleep(10000); // ~10s to make users able to read the CLI.
2422
+ try {
2423
+ // Automatically open the page (# Step 2).
2424
+ await open(OAuthDeviceCode.verification_uri);
2425
+ }
2426
+ catch (error) {
2427
+ console.log(`${theme.symbols.info} Please authenticate via GitHub at ${OAuthDeviceCode.verification_uri}`);
2428
+ }
2429
+ spinner.stop();
2430
+ };
2431
+ /**
2432
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
2433
+ * @param clientId <string> - The client id of the Auth0 application.
2434
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
2435
+ * @returns <string> - The token to sign in to Firebase
2436
+ */
2437
+ const executeSIWEDeviceFlow = async (clientId, firebaseFunctions) => {
2438
+ // Call Auth0 endpoint to request device code uri
2439
+ const OAuthDeviceCode = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
2440
+ method: "POST",
2441
+ headers: { "content-type": "application/json" },
2442
+ body: JSON.stringify({
2443
+ client_id: clientId,
2444
+ scope: "openid",
2445
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
2446
+ })
2447
+ }).then((_res) => _res.json()));
2448
+ await showVerificationCodeAndUri(OAuthDeviceCode);
2449
+ // Poll Auth0 endpoint until you get token or request expires
2450
+ let isSignedIn = false;
2451
+ let isExpired = false;
2452
+ let auth0Token = "";
2453
+ while (!isSignedIn && !isExpired) {
2454
+ // Call Auth0 endpoint to request token
2455
+ const OAuthToken = (await fetch$1(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
2456
+ method: "POST",
2457
+ headers: { "content-type": "application/json" },
2458
+ body: JSON.stringify({
2459
+ client_id: clientId,
2460
+ device_code: OAuthDeviceCode.device_code,
2461
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2462
+ })
2463
+ }).then((_res) => _res.json()));
2464
+ if (OAuthToken.error) {
2465
+ if (OAuthToken.error === "authorization_pending") {
2466
+ // Wait for the user to sign in
2467
+ await sleep(OAuthDeviceCode.interval * 1000);
2468
+ }
2469
+ else if (OAuthToken.error === "slow_down") {
2470
+ // Wait for the user to sign in
2471
+ await sleep(OAuthDeviceCode.interval * 1000 * 2);
2472
+ }
2473
+ else if (OAuthToken.error === "expired_token") {
2474
+ // The user didn't sign in on time
2475
+ isExpired = true;
2476
+ }
2477
+ }
2478
+ else {
2479
+ // The user signed in
2480
+ isSignedIn = true;
2481
+ auth0Token = OAuthToken.access_token;
2482
+ }
2483
+ }
2484
+ // Send token to cloud function to check nonce, create user and retrieve token
2485
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress);
2332
2486
  const result = await cf({
2333
- proof,
2334
- publicSignals
2487
+ auth0Token
2335
2488
  });
2336
- const { valid, token, message } = result.data;
2489
+ const { token, valid, message } = result.data;
2337
2490
  if (!valid) {
2338
2491
  showError(message, true);
2492
+ deleteLocalAuthMethod();
2493
+ deleteLocalAccessToken();
2494
+ }
2495
+ return token;
2496
+ };
2497
+ /**
2498
+ * Auth command using Sign In With Ethereum
2499
+ * @notice The auth command allows a user to make the association of their Ethereum account with the CLI by leveraging SIWE as an authentication mechanism.
2500
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
2501
+ */
2502
+ const authSIWE = async () => {
2503
+ try {
2504
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2505
+ // Console more context for the user.
2506
+ console.log(`${theme.symbols.info} ${theme.text.bold(`You are about to authenticate on this CLI using your Ethereum address (device flow - OAuth 2.0 mechanism).\n${theme.symbols.warning} Please, note that only a Sign-in With Ethereum signature will be required`)}\n`);
2507
+ const spinner = customSpinner(`Checking authentication token...`, `clock`);
2508
+ spinner.start();
2509
+ await sleep(5000);
2510
+ // Manage OAuth Github or SIWE token.
2511
+ const isLocalTokenStored = checkLocalAccessToken();
2512
+ if (!isLocalTokenStored) {
2513
+ spinner.fail(`No local authentication token found\n`);
2514
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
2515
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions);
2516
+ // Store the new access token.
2517
+ setLocalAuthMethod("siwe");
2518
+ setLocalAccessToken(newToken);
2519
+ }
2520
+ else
2521
+ spinner.succeed(`Local authentication token found\n`);
2522
+ // Get access token from local store.
2523
+ const token = String(getLocalAccessToken());
2524
+ spinner.text = `Authenticating...`;
2525
+ spinner.start();
2526
+ // Exchange token for credential.
2527
+ const credentials = await signInWithCustomToken(getAuth(), token);
2528
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`);
2529
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2530
+ process.exit(0);
2531
+ }
2532
+ catch (error) {
2533
+ // Delete local token.
2534
+ console.log("An error crashed the process. Deleting local token and identity.");
2535
+ console.error(error);
2536
+ deleteLocalAuthMethod();
2537
+ deleteLocalAccessToken();
2339
2538
  }
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
2539
  };
2350
2540
 
2351
2541
  /**
@@ -2470,8 +2660,8 @@ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, cer
2470
2660
  spinner.fail(`You may not have enough memory to calculate the contribution for the Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}.\n\n${theme.symbols.info} The required amount of disk space is ${contributionDiskSpaceRequirement < 0.01
2471
2661
  ? theme.text.bold(`< 0.01`)
2472
2662
  : theme.text.bold(contributionDiskSpaceRequirement)} GB but you only have ${participantFreeDiskSpace > 0 ? theme.text.bold(participantFreeDiskSpace.toFixed(2)) : theme.text.bold(0)} GB available memory \nThe estimate ${theme.text.bold("may not be 100% correct")} since is based on the aggregate free memory on your disks but some may not be detected!\n`);
2473
- const { confirmation } = await askForConfirmation(`Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`, "Continue", "Exit");
2474
- wannaContributeOrHaveEnoughMemory = !!confirmation;
2663
+ const { confirmationEnoughMemory } = await askForConfirmation(`Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`, "Continue", "Exit");
2664
+ wannaContributeOrHaveEnoughMemory = !!confirmationEnoughMemory;
2475
2665
  if (circuitSequencePosition > 1) {
2476
2666
  console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2477
2667
  // Asks the contributor if their wants to terminate contributions for the ceremony.
@@ -2540,8 +2730,8 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2540
2730
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2541
2731
  await sleep(1000); // workaround for file descriptor unexpected close.
2542
2732
  let gistUrl = "";
2543
- const isBandada = checkLocalBandadaIdentity();
2544
- if (!isBandada) {
2733
+ const isGithub = getLocalAuthMethod() === "github";
2734
+ if (isGithub) {
2545
2735
  gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2546
2736
  console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2547
2737
  }
@@ -2597,6 +2787,7 @@ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, p
2597
2787
  }
2598
2788
  });
2599
2789
  };
2790
+ let contributionInProgress = false;
2600
2791
  /**
2601
2792
  * Listen to current authenticated participant document changes.
2602
2793
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -2724,11 +2915,21 @@ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFuncti
2724
2915
  (!noTemporaryContributionData && resumingWithSameTemporaryData);
2725
2916
  // Scenario (3.B).
2726
2917
  if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
2918
+ if (contributionInProgress) {
2919
+ console.warn(`\n${theme.symbols.warning} Received instruction to start/resume contribution but contribution is already in progress...[skipping]`);
2920
+ return;
2921
+ }
2727
2922
  // Communicate resume / start of the contribution to participant.
2728
2923
  await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2729
- // Start / Resume the contribution for the participant.
2730
- await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2731
- circuits.length);
2924
+ try {
2925
+ contributionInProgress = true;
2926
+ // Start / Resume the contribution for the participant.
2927
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false, // not finalizing.
2928
+ circuits.length);
2929
+ }
2930
+ finally {
2931
+ contributionInProgress = false;
2932
+ }
2732
2933
  }
2733
2934
  // Scenario (3.A).
2734
2935
  else if (isWaitingForContribution)
@@ -3259,6 +3460,7 @@ const logout = async () => {
3259
3460
  const auth = getAuth();
3260
3461
  await signOut(auth);
3261
3462
  // Delete local token.
3463
+ deleteLocalAuthMethod();
3262
3464
  deleteLocalAccessToken();
3263
3465
  deleteLocalBandadaIdentity();
3264
3466
  await sleep(3000); // ~3s.
@@ -3365,6 +3567,10 @@ program
3365
3567
  .command("auth-bandada")
3366
3568
  .description("authenticate yourself in a privacy-perserving manner using Bandada")
3367
3569
  .action(authBandada);
3570
+ program
3571
+ .command("auth-siwe")
3572
+ .description("authenticate yourself using your Ethereum account (Sign In With Ethereum - SIWE)")
3573
+ .action(authSIWE);
3368
3574
  program
3369
3575
  .command("contribute")
3370
3576
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Auth command using Sign In With Ethereum
3
+ * @notice The auth command allows a user to make the association of their Ethereum account with the CLI by leveraging SIWE as an authentication mechanism.
4
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
5
+ */
6
+ declare const authSIWE: () => Promise<void>;
7
+ export default authSIWE;
@@ -1,6 +1,7 @@
1
1
  export { default as setup } from "./setup.js";
2
2
  export { default as auth } from "./auth.js";
3
3
  export { default as authBandada } from "./authBandada.js";
4
+ export { default as authSIWE } from "./authSIWE.js";
4
5
  export { default as contribute } from "./contribute.js";
5
6
  export { default as observe } from "./observe.js";
6
7
  export { default as finalize } from "./finalize.js";
@@ -54,6 +54,25 @@ export declare const setLocalBandadaIdentity: (identity: string) => void;
54
54
  * Delete the stored Bandada identity.
55
55
  */
56
56
  export declare const deleteLocalBandadaIdentity: () => void;
57
+ /**
58
+ * Return the authentication method, if present.
59
+ * @returns <string | undefined> - the authentication method if present, otherwise undefined.
60
+ */
61
+ export declare const getLocalAuthMethod: () => string | unknown;
62
+ /**
63
+ * Check if the authentication method exists in the local storage.
64
+ * @returns <boolean>
65
+ */
66
+ export declare const checkLocalAuthMethod: () => boolean;
67
+ /**
68
+ * Set the authentication method.
69
+ * @param method <string> - the authentication method to be stored.
70
+ */
71
+ export declare const setLocalAuthMethod: (method: string) => void;
72
+ /**
73
+ * Delete the stored authentication method.
74
+ */
75
+ export declare const deleteLocalAuthMethod: () => void;
57
76
  /**
58
77
  * Get the complete local file path.
59
78
  * @param cwd <string> - the current working directory path.
@@ -75,3 +75,54 @@ export type VerifiedBandadaResponse = {
75
75
  message: string;
76
76
  token: string;
77
77
  };
78
+ /**
79
+ * Define the return object of the device code uri request.
80
+ * @typedef {Object} OAuthDeviceCodeResponse
81
+ * @property {string} device_code - the device code.
82
+ * @property {string} user_code - the user code.
83
+ * @property {string} verification_uri - the verification uri.
84
+ * @property {number} expires_in - the expiration time in seconds.
85
+ * @property {number} interval - the interval time in seconds.
86
+ * @property {string} verification_uri_complete - the complete verification uri.
87
+ */
88
+ export type OAuthDeviceCodeResponse = {
89
+ device_code: string;
90
+ user_code: string;
91
+ verification_uri: string;
92
+ expires_in: number;
93
+ interval: number;
94
+ verification_uri_complete: string;
95
+ };
96
+ /**
97
+ * Define the return object of the polling endpoint
98
+ * @typedef {Object} OAuthTokenResponse
99
+ * @property {string} access_token - the resulting device flow token
100
+ * @property {string} token_type - token type
101
+ * @property {number} expires_in - when does the token expires
102
+ * @property {string} scope - the scope requested by the initial device flow endpoint
103
+ * @property {string} refresh_token - refresh token
104
+ * @property {string} id_token - id token
105
+ * @property {string} error - in case there was an error
106
+ * @property {string} error_description - error details
107
+ */
108
+ export type OAuthTokenResponse = {
109
+ access_token: string;
110
+ token_type: string;
111
+ expires_in: number;
112
+ scope: string;
113
+ refresh_token: string;
114
+ id_token: string;
115
+ error?: string;
116
+ error_description?: string;
117
+ };
118
+ /**
119
+ * @typedef {Object} CheckNonceOfSIWEAddressResponse
120
+ * @property {boolean} valid - if the checking was valid or not
121
+ * @property {string} message - more information about the validity
122
+ * @property {string} token - token to sign into Firebase
123
+ */
124
+ export type CheckNonceOfSIWEAddressResponse = {
125
+ valid: boolean;
126
+ message: string;
127
+ token: string;
128
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devtion/devcli",
3
3
  "type": "module",
4
- "version": "0.0.0-57a8ab9",
4
+ "version": "0.0.0-671e653",
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,6 +34,7 @@
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:siwe": "yarn start auth-siwe",
37
38
  "auth:bandada": "yarn start auth-bandada",
38
39
  "contribute": "yarn start contribute",
39
40
  "clean": "yarn start clean",
@@ -59,6 +60,7 @@
59
60
  "@types/winston": "^2.4.4",
60
61
  "rollup-plugin-auto-external": "^2.0.0",
61
62
  "rollup-plugin-cleanup": "^3.2.1",
63
+ "rollup-plugin-copy": "^3.5.0",
62
64
  "rollup-plugin-typescript2": "^0.34.1",
63
65
  "solc": "^0.8.19",
64
66
  "ts-node": "^10.9.1",
@@ -102,5 +104,5 @@
102
104
  "publishConfig": {
103
105
  "access": "public"
104
106
  },
105
- "gitHead": "6bddf60f1121786c19ad4437e0448de4c859b829"
107
+ "gitHead": "c9c799447bc2e3abd6b665fe09622ac724ea9b79"
106
108
  }
@@ -8,7 +8,12 @@ import figlet from "figlet"
8
8
  import { fileURLToPath } from "url"
9
9
  import { dirname } from "path"
10
10
  import { GENERIC_ERRORS, showError } from "../lib/errors.js"
11
- import { checkLocalAccessToken, getLocalAccessToken, setLocalAccessToken } from "../lib/localConfigs.js"
11
+ import {
12
+ checkLocalAccessToken,
13
+ getLocalAccessToken,
14
+ setLocalAccessToken,
15
+ setLocalAuthMethod
16
+ } from "../lib/localConfigs.js"
12
17
  import { bootstrapCommandExecutionAndServices, signInToFirebase } from "../lib/services.js"
13
18
  import theme from "../lib/theme.js"
14
19
  import {
@@ -171,6 +176,7 @@ const auth = async () => {
171
176
  const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID))
172
177
 
173
178
  // Store the new access token.
179
+ setLocalAuthMethod("github")
174
180
  setLocalAccessToken(newToken)
175
181
  } else spinner.succeed(`Local authentication token found\n`)
176
182
 
@@ -3,8 +3,9 @@ import { Identity } from "@semaphore-protocol/identity"
3
3
  import { commonTerms } from "@devtion/actions"
4
4
  import { httpsCallable } from "firebase/functions"
5
5
  import { groth16 } from "snarkjs"
6
- import path from "path"
7
6
  import { getAuth, signInWithCustomToken } from "firebase/auth"
7
+ import prompts from "prompts"
8
+ import { getLocalDirname } from "../lib/files.js"
8
9
  import theme from "../lib/theme.js"
9
10
  import { customSpinner } from "../lib/utils.js"
10
11
  import { VerifiedBandadaResponse } from "../types/index.js"
@@ -13,85 +14,105 @@ import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
13
14
  import { addMemberToGroup, isGroupMember } from "../lib/bandada.js"
14
15
  import {
15
16
  checkLocalBandadaIdentity,
17
+ deleteLocalAccessToken,
18
+ deleteLocalAuthMethod,
19
+ deleteLocalBandadaIdentity,
16
20
  getLocalBandadaIdentity,
17
21
  setLocalAccessToken,
22
+ setLocalAuthMethod,
18
23
  setLocalBandadaIdentity
19
24
  } from "../lib/localConfigs.js"
20
- import prompts from "prompts"
21
25
 
22
26
  const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env
23
27
 
24
28
  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)
29
+ try {
30
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices()
31
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`)
32
+ spinner.start()
33
+ // 1. check if _identity string exists in local storage
34
+ let identityString: string | unknown
35
+ const isIdentityStringStored = checkLocalBandadaIdentity()
36
+ if (isIdentityStringStored) {
37
+ identityString = getLocalBandadaIdentity()
38
+ spinner.succeed(`Identity seed found\n`)
39
+ } else {
40
+ spinner.warn(`Identity seed not found\n`)
41
+ // 2. generate a random _identity string and save it in local storage
42
+ const { seed } = await prompts({
43
+ type: "text",
44
+ name: "seed",
45
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
46
+ initial: false
47
+ })
48
+ identityString = seed as string
49
+ setLocalBandadaIdentity(identityString as string)
50
+ }
51
+ // 3. create a semaphore identity with _identity string as a seed
52
+ const identity = new Identity(identityString as string)
48
53
 
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
- }
54
+ // 4. check if the user is a member of the group
55
+ console.log(`Checking Bandada membership...`)
56
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity)
57
+ if (!isMember) {
58
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity)
59
+ }
55
60
 
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)}.`)
61
+ // 5. generate a proof that the user owns the commitment.
62
+ spinner.text = `Generating proof of identity...`
63
+ spinner.start()
64
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
65
+
66
+ const initDirectoryName = getLocalDirname()
67
+ const directoryName = initDirectoryName.includes("/src") ? "." : initDirectoryName
89
68
 
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
- )
69
+ const { proof, publicSignals } = await groth16.fullProve(
70
+ {
71
+ identityTrapdoor: identity.trapdoor,
72
+ identityNullifier: identity.nullifier,
73
+ externalNullifier: BANDADA_GROUP_ID
74
+ },
75
+ `${directoryName}/public/mini-semaphore.wasm`,
76
+ `${directoryName}/public/mini-semaphore.zkey`
77
+ )
78
+ spinner.succeed(`Proof generated.\n`)
79
+ spinner.text = `Sending proof to verification...`
80
+ spinner.start()
81
+ // 6. send proof to a cloud function that verifies it and checks membership
82
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof)
83
+ const result = await cf({
84
+ proof,
85
+ publicSignals
86
+ })
87
+ const { valid, token, message } = result.data as VerifiedBandadaResponse
88
+ if (!valid) {
89
+ showError(message, true)
90
+ deleteLocalAuthMethod()
91
+ deleteLocalAccessToken()
92
+ deleteLocalBandadaIdentity()
93
+ }
94
+ spinner.succeed(`Proof verified.\n`)
95
+ spinner.text = `Authenticating...`
96
+ spinner.start()
97
+ // 7. Auth to p0tion firebase
98
+ const credentials = await signInWithCustomToken(getAuth(), token)
99
+ setLocalAuthMethod("bandada")
100
+ setLocalAccessToken(token)
101
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`)
102
+
103
+ console.log(
104
+ `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
105
+ `phase2cli logout`
106
+ )} command`
107
+ )
108
+ } catch (error) {
109
+ // Delete local token.
110
+ console.log("An error crashed the process. Deleting local token and identity.")
111
+ console.error(error)
112
+ deleteLocalAuthMethod()
113
+ deleteLocalAccessToken()
114
+ deleteLocalBandadaIdentity()
115
+ }
95
116
 
96
117
  process.exit(0)
97
118
  }
@@ -0,0 +1,178 @@
1
+ import open from "open"
2
+ import figlet from "figlet"
3
+ import clipboard from "clipboardy"
4
+ import fetch from "node-fetch"
5
+ import { getAuth, signInWithCustomToken } from "firebase/auth"
6
+ import { httpsCallable } from "firebase/functions"
7
+ import { commonTerms } from "@devtion/actions"
8
+ import { showError } from "../lib/errors.js"
9
+ import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
10
+ import theme from "../lib/theme.js"
11
+ import { customSpinner, sleep } from "../lib/utils.js"
12
+ import { CheckNonceOfSIWEAddressResponse, OAuthDeviceCodeResponse, OAuthTokenResponse } from "../types/index.js"
13
+ import {
14
+ checkLocalAccessToken,
15
+ deleteLocalAccessToken,
16
+ deleteLocalAuthMethod,
17
+ getLocalAccessToken,
18
+ setLocalAccessToken,
19
+ setLocalAuthMethod
20
+ } from "../lib/localConfigs.js"
21
+
22
+ const showVerificationCodeAndUri = async (OAuthDeviceCode: OAuthDeviceCodeResponse) => {
23
+ // Copy code to clipboard.
24
+ let noClipboard = false
25
+ try {
26
+ clipboard.writeSync(OAuthDeviceCode.user_code)
27
+ clipboard.readSync()
28
+ } catch (error) {
29
+ noClipboard = true
30
+ }
31
+ // Display data.
32
+ console.log(
33
+ `${theme.symbols.warning} Visit ${theme.text.bold(
34
+ theme.text.underlined(OAuthDeviceCode.verification_uri)
35
+ )} on this device to generate a new token and authenticate\n`
36
+ )
37
+ console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n")
38
+
39
+ const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``
40
+ console.log(
41
+ `${theme.symbols.info} Your auth code: ${theme.text.bold(OAuthDeviceCode.user_code)} ${message} ${
42
+ theme.symbols.success
43
+ }\n`
44
+ )
45
+ const spinner = customSpinner(`Redirecting to Github...`, `clock`)
46
+ spinner.start()
47
+ await sleep(10000) // ~10s to make users able to read the CLI.
48
+ try {
49
+ // Automatically open the page (# Step 2).
50
+ await open(OAuthDeviceCode.verification_uri)
51
+ } catch (error: any) {
52
+ console.log(`${theme.symbols.info} Please authenticate via GitHub at ${OAuthDeviceCode.verification_uri}`)
53
+ }
54
+ spinner.stop()
55
+ }
56
+
57
+ /**
58
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
59
+ * @param clientId <string> - The client id of the Auth0 application.
60
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
61
+ * @returns <string> - The token to sign in to Firebase
62
+ */
63
+ const executeSIWEDeviceFlow = async (clientId: string, firebaseFunctions: any): Promise<string> => {
64
+ // Call Auth0 endpoint to request device code uri
65
+ const OAuthDeviceCode = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
66
+ method: "POST",
67
+ headers: { "content-type": "application/json" },
68
+ body: JSON.stringify({
69
+ client_id: clientId,
70
+ scope: "openid",
71
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
72
+ })
73
+ }).then((_res) => _res.json())) as OAuthDeviceCodeResponse
74
+ await showVerificationCodeAndUri(OAuthDeviceCode)
75
+ // Poll Auth0 endpoint until you get token or request expires
76
+ let isSignedIn = false
77
+ let isExpired = false
78
+ let auth0Token = ""
79
+ while (!isSignedIn && !isExpired) {
80
+ // Call Auth0 endpoint to request token
81
+ const OAuthToken = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
82
+ method: "POST",
83
+ headers: { "content-type": "application/json" },
84
+ body: JSON.stringify({
85
+ client_id: clientId,
86
+ device_code: OAuthDeviceCode.device_code,
87
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
88
+ })
89
+ }).then((_res) => _res.json())) as OAuthTokenResponse
90
+ if (OAuthToken.error) {
91
+ if (OAuthToken.error === "authorization_pending") {
92
+ // Wait for the user to sign in
93
+ await sleep(OAuthDeviceCode.interval * 1000)
94
+ } else if (OAuthToken.error === "slow_down") {
95
+ // Wait for the user to sign in
96
+ await sleep(OAuthDeviceCode.interval * 1000 * 2)
97
+ } else if (OAuthToken.error === "expired_token") {
98
+ // The user didn't sign in on time
99
+ isExpired = true
100
+ }
101
+ } else {
102
+ // The user signed in
103
+ isSignedIn = true
104
+ auth0Token = OAuthToken.access_token
105
+ }
106
+ }
107
+ // Send token to cloud function to check nonce, create user and retrieve token
108
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress)
109
+ const result = await cf({
110
+ auth0Token
111
+ })
112
+ const { token, valid, message } = result.data as CheckNonceOfSIWEAddressResponse
113
+ if (!valid) {
114
+ showError(message, true)
115
+ deleteLocalAuthMethod()
116
+ deleteLocalAccessToken()
117
+ }
118
+ return token
119
+ }
120
+
121
+ /**
122
+ * Auth command using Sign In With Ethereum
123
+ * @notice The auth command allows a user to make the association of their Ethereum account with the CLI by leveraging SIWE as an authentication mechanism.
124
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
125
+ */
126
+ const authSIWE = async () => {
127
+ try {
128
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices()
129
+ // Console more context for the user.
130
+ console.log(
131
+ `${theme.symbols.info} ${theme.text.bold(
132
+ `You are about to authenticate on this CLI using your Ethereum address (device flow - OAuth 2.0 mechanism).\n${theme.symbols.warning} Please, note that only a Sign-in With Ethereum signature will be required`
133
+ )}\n`
134
+ )
135
+ const spinner = customSpinner(`Checking authentication token...`, `clock`)
136
+ spinner.start()
137
+ await sleep(5000)
138
+
139
+ // Manage OAuth Github or SIWE token.
140
+ const isLocalTokenStored = checkLocalAccessToken()
141
+
142
+ if (!isLocalTokenStored) {
143
+ spinner.fail(`No local authentication token found\n`)
144
+
145
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
146
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions)
147
+
148
+ // Store the new access token.
149
+ setLocalAuthMethod("siwe")
150
+ setLocalAccessToken(newToken)
151
+ } else spinner.succeed(`Local authentication token found\n`)
152
+
153
+ // Get access token from local store.
154
+ const token = String(getLocalAccessToken())
155
+
156
+ spinner.text = `Authenticating...`
157
+ spinner.start()
158
+
159
+ // Exchange token for credential.
160
+ const credentials = await signInWithCustomToken(getAuth(), token)
161
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`)
162
+
163
+ console.log(
164
+ `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
165
+ `phase2cli logout`
166
+ )} command`
167
+ )
168
+ process.exit(0)
169
+ } catch (error) {
170
+ // Delete local token.
171
+ console.log("An error crashed the process. Deleting local token and identity.")
172
+ console.error(error)
173
+ deleteLocalAuthMethod()
174
+ deleteLocalAccessToken()
175
+ }
176
+ }
177
+
178
+ export default authSIWE
@@ -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 { checkLocalBandadaIdentity, getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
44
+ import { getAttestationLocalFilePath, getLocalAuthMethod, localPaths } from "../lib/localConfigs.js"
45
45
  import theme from "../lib/theme.js"
46
46
  import { checkAndMakeNewDirectoryIfNonexistent, writeFile } from "../lib/files.js"
47
47
 
@@ -281,12 +281,12 @@ export const handleDiskSpaceRequirementForNextContribution = async (
281
281
  )} since is based on the aggregate free memory on your disks but some may not be detected!\n`
282
282
  )
283
283
 
284
- const { confirmation } = await askForConfirmation(
284
+ const { confirmationEnoughMemory } = await askForConfirmation(
285
285
  `Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`,
286
286
  "Continue",
287
287
  "Exit"
288
288
  )
289
- wannaContributeOrHaveEnoughMemory = !!confirmation
289
+ wannaContributeOrHaveEnoughMemory = !!confirmationEnoughMemory
290
290
 
291
291
  if (circuitSequencePosition > 1) {
292
292
  console.log(
@@ -420,8 +420,8 @@ export const handlePublicAttestation = async (
420
420
  await sleep(1000) // workaround for file descriptor unexpected close.
421
421
 
422
422
  let gistUrl = ""
423
- const isBandada = checkLocalBandadaIdentity()
424
- if (!isBandada) {
423
+ const isGithub = getLocalAuthMethod() === "github"
424
+ if (isGithub) {
425
425
  gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
426
426
 
427
427
  console.log(
@@ -519,6 +519,8 @@ export const listenToCeremonyCircuitDocumentChanges = (
519
519
  })
520
520
  }
521
521
 
522
+ let contributionInProgress = false
523
+
522
524
  /**
523
525
  * Listen to current authenticated participant document changes.
524
526
  * @dev this is the core business logic related to the execution of the contribute command.
@@ -711,6 +713,12 @@ export const listenToParticipantDocumentChanges = async (
711
713
 
712
714
  // Scenario (3.B).
713
715
  if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
716
+ if (contributionInProgress) {
717
+ console.warn(
718
+ `\n${theme.symbols.warning} Received instruction to start/resume contribution but contribution is already in progress...[skipping]`
719
+ )
720
+ return
721
+ }
714
722
  // Communicate resume / start of the contribution to participant.
715
723
  await simpleLoader(
716
724
  `${
@@ -720,18 +728,24 @@ export const listenToParticipantDocumentChanges = async (
720
728
  3000
721
729
  )
722
730
 
723
- // Start / Resume the contribution for the participant.
724
- await handleStartOrResumeContribution(
725
- cloudFunctions,
726
- firestoreDatabase,
727
- ceremony,
728
- circuit,
729
- participant,
730
- entropy,
731
- providerUserId,
732
- false, // not finalizing.
733
- circuits.length
734
- )
731
+ try {
732
+ contributionInProgress = true
733
+
734
+ // Start / Resume the contribution for the participant.
735
+ await handleStartOrResumeContribution(
736
+ cloudFunctions,
737
+ firestoreDatabase,
738
+ ceremony,
739
+ circuit,
740
+ participant,
741
+ entropy,
742
+ providerUserId,
743
+ false, // not finalizing.
744
+ circuits.length
745
+ )
746
+ } finally {
747
+ contributionInProgress = false
748
+ }
735
749
  }
736
750
  // Scenario (3.A).
737
751
  else if (isWaitingForContribution)
@@ -1,6 +1,7 @@
1
1
  export { default as setup } from "./setup.js"
2
2
  export { default as auth } from "./auth.js"
3
3
  export { default as authBandada } from "./authBandada.js"
4
+ export { default as authSIWE } from "./authSIWE.js"
4
5
  export { default as contribute } from "./contribute.js"
5
6
  export { default as observe } from "./observe.js"
6
7
  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, deleteLocalBandadaIdentity } from "../lib/localConfigs.js"
9
+ import { deleteLocalAccessToken, deleteLocalAuthMethod, deleteLocalBandadaIdentity } from "../lib/localConfigs.js"
10
10
 
11
11
  /**
12
12
  * Logout command.
@@ -52,6 +52,7 @@ const logout = async () => {
52
52
  await signOut(auth)
53
53
 
54
54
  // Delete local token.
55
+ deleteLocalAuthMethod()
55
56
  deleteLocalAccessToken()
56
57
  deleteLocalBandadaIdentity()
57
58
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { fileURLToPath } from "url"
7
7
  import {
8
8
  setup,
9
9
  auth,
10
+ authSIWE,
10
11
  authBandada,
11
12
  contribute,
12
13
  observe,
@@ -32,6 +33,10 @@ program
32
33
  .command("auth-bandada")
33
34
  .description("authenticate yourself in a privacy-perserving manner using Bandada")
34
35
  .action(authBandada)
36
+ program
37
+ .command("auth-siwe")
38
+ .description("authenticate yourself using your Ethereum account (Sign In With Ethereum - SIWE)")
39
+ .action(authSIWE)
35
40
  program
36
41
  .command("contribute")
37
42
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -28,6 +28,10 @@ const config = new Conf({
28
28
  bandadaIdentity: {
29
29
  type: "string",
30
30
  default: ""
31
+ },
32
+ authMethod: {
33
+ type: "string",
34
+ default: ""
31
35
  }
32
36
  }
33
37
  })
@@ -118,6 +122,29 @@ export const setLocalBandadaIdentity = (identity: string) => config.set("bandada
118
122
  */
119
123
  export const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity")
120
124
 
125
+ /**
126
+ * Return the authentication method, if present.
127
+ * @returns <string | undefined> - the authentication method if present, otherwise undefined.
128
+ */
129
+ export const getLocalAuthMethod = (): string | unknown => config.get("authMethod")
130
+
131
+ /**
132
+ * Check if the authentication method exists in the local storage.
133
+ * @returns <boolean>
134
+ */
135
+ export const checkLocalAuthMethod = (): boolean => config.has("authMethod") && !!config.get("authMethod")
136
+
137
+ /**
138
+ * Set the authentication method.
139
+ * @param method <string> - the authentication method to be stored.
140
+ */
141
+ export const setLocalAuthMethod = (method: string) => config.set("authMethod", method)
142
+
143
+ /**
144
+ * Delete the stored authentication method.
145
+ */
146
+ export const deleteLocalAuthMethod = () => config.delete("authMethod")
147
+
121
148
  /**
122
149
  * Get the complete local file path.
123
150
  * @param cwd <string> - the current working directory path.
@@ -14,9 +14,9 @@ import { AuthUser } from "../types/index.js"
14
14
  import { CONFIG_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
15
15
  import {
16
16
  checkLocalAccessToken,
17
- checkLocalBandadaIdentity,
18
17
  deleteLocalAccessToken,
19
- getLocalAccessToken
18
+ getLocalAccessToken,
19
+ getLocalAuthMethod
20
20
  } from "./localConfigs.js"
21
21
  import theme from "./theme.js"
22
22
  import { exchangeGithubTokenForCredentials, getGithubProviderUserId, getUserHandleFromProviderUserId } from "./utils.js"
@@ -171,21 +171,33 @@ export const checkAuth = async (firebaseApp: FirebaseApp): Promise<AuthUser> =>
171
171
 
172
172
  let providerUserId: string
173
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)
174
+ const authMethod = getLocalAuthMethod()
175
+ switch (authMethod) {
176
+ case "github": {
177
+ // Get credentials.
178
+ const credentials = exchangeGithubTokenForCredentials(token)
179
+ // Sign in to Firebase using credentials.
180
+ await signInToFirebase(firebaseApp, credentials)
181
+ // Get Github unique identifier (handle-id).
182
+ providerUserId = await getGithubProviderUserId(String(token))
183
+ username = getUserHandleFromProviderUserId(providerUserId)
184
+ break
185
+ }
186
+ case "bandada": {
187
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
188
+ providerUserId = userCredentials.user.uid
189
+ username = providerUserId
190
+ break
191
+ }
192
+ case "siwe": {
193
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
194
+ providerUserId = userCredentials.user.uid
195
+ username = providerUserId
196
+ break
197
+ }
198
+ default: {
199
+ break
200
+ }
189
201
  }
190
202
 
191
203
  // Get current authenticated user.
@@ -81,3 +81,58 @@ export type VerifiedBandadaResponse = {
81
81
  message: string
82
82
  token: string
83
83
  }
84
+
85
+ /**
86
+ * Define the return object of the device code uri request.
87
+ * @typedef {Object} OAuthDeviceCodeResponse
88
+ * @property {string} device_code - the device code.
89
+ * @property {string} user_code - the user code.
90
+ * @property {string} verification_uri - the verification uri.
91
+ * @property {number} expires_in - the expiration time in seconds.
92
+ * @property {number} interval - the interval time in seconds.
93
+ * @property {string} verification_uri_complete - the complete verification uri.
94
+ */
95
+ export type OAuthDeviceCodeResponse = {
96
+ device_code: string
97
+ user_code: string
98
+ verification_uri: string
99
+ expires_in: number
100
+ interval: number
101
+ verification_uri_complete: string
102
+ }
103
+
104
+ /**
105
+ * Define the return object of the polling endpoint
106
+ * @typedef {Object} OAuthTokenResponse
107
+ * @property {string} access_token - the resulting device flow token
108
+ * @property {string} token_type - token type
109
+ * @property {number} expires_in - when does the token expires
110
+ * @property {string} scope - the scope requested by the initial device flow endpoint
111
+ * @property {string} refresh_token - refresh token
112
+ * @property {string} id_token - id token
113
+ * @property {string} error - in case there was an error
114
+ * @property {string} error_description - error details
115
+ */
116
+ export type OAuthTokenResponse = {
117
+ access_token: string
118
+ token_type: string
119
+ expires_in: number
120
+ scope: string
121
+ refresh_token: string
122
+ id_token: string
123
+ // error response should contain
124
+ error?: string
125
+ error_description?: string
126
+ }
127
+
128
+ /**
129
+ * @typedef {Object} CheckNonceOfSIWEAddressResponse
130
+ * @property {boolean} valid - if the checking was valid or not
131
+ * @property {string} message - more information about the validity
132
+ * @property {string} token - token to sign into Firebase
133
+ */
134
+ export type CheckNonceOfSIWEAddressResponse = {
135
+ valid: boolean
136
+ message: string
137
+ token: string
138
+ }