@devtion/devcli 0.0.0-57a8ab9 → 0.0.0-5fad82d

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.
@@ -27,7 +27,7 @@ export declare const handleAdditionOfCircuitsToCeremony: (r1csOptions: Array<str
27
27
  export declare const displayCeremonySummary: (ceremonyInputData: CeremonyInputData, circuits: Array<CircuitDocument>) => void;
28
28
  /**
29
29
  * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
30
- * @dev we are downloading the Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup.
30
+ * @dev we are downloading the Powers of Tau file from Perpetual Powers of Tau Phase 1 Trusted Setup.
31
31
  * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
32
32
  * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
33
33
  * @returns <Promise<void>>
@@ -76,7 +76,7 @@ export declare const handleCircuitArtifactUploadToStorage: (firebaseFunctions: F
76
76
  * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
77
77
  * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
78
78
  * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
79
- * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
79
+ * from PPoT ceremony Phase 1 Setup Ceremony.
80
80
  * @param cmd? <any> - the path to the ceremony setup file.
81
81
  */
82
82
  declare const setup: (cmd: {
@@ -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.
@@ -90,7 +90,7 @@ export declare const promptPotSelector: (options: Array<string>) => Promise<stri
90
90
  * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
91
91
  * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
92
92
  */
93
- export declare const promptForCeremonySelection: (ceremoniesDocuments: Array<FirebaseDocumentInfo>, isFinalizing: boolean) => Promise<FirebaseDocumentInfo>;
93
+ export declare const promptForCeremonySelection: (ceremoniesDocuments: Array<FirebaseDocumentInfo>, isFinalizing: boolean, messageToDisplay?: string) => Promise<FirebaseDocumentInfo>;
94
94
  /**
95
95
  * Prompt the participant to type the entropy or the coordinator to type the beacon.
96
96
  * @param isEntropy <boolean> - true when prompting for typing entropy; otherwise false.
@@ -75,3 +75,60 @@ 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
+ * @property {string} error - in case there was an error, show the code
88
+ * @property {string} error_description - error details.
89
+ * @property {string} error_uri - error uri.
90
+ */
91
+ export type OAuthDeviceCodeResponse = {
92
+ device_code: string;
93
+ user_code: string;
94
+ verification_uri: string;
95
+ expires_in: number;
96
+ interval: number;
97
+ verification_uri_complete: string;
98
+ error?: string;
99
+ error_description?: string;
100
+ error_uri?: string;
101
+ };
102
+ /**
103
+ * Define the return object of the polling endpoint
104
+ * @typedef {Object} OAuthTokenResponse
105
+ * @property {string} access_token - the resulting device flow token
106
+ * @property {string} token_type - token type
107
+ * @property {number} expires_in - when does the token expires
108
+ * @property {string} scope - the scope requested by the initial device flow endpoint
109
+ * @property {string} refresh_token - refresh token
110
+ * @property {string} id_token - id token
111
+ * @property {string} error - in case there was an error, show the code
112
+ * @property {string} error_description - error details
113
+ */
114
+ export type OAuthTokenResponse = {
115
+ access_token: string;
116
+ token_type: string;
117
+ expires_in: number;
118
+ scope: string;
119
+ refresh_token: string;
120
+ id_token: string;
121
+ error?: string;
122
+ error_description?: string;
123
+ };
124
+ /**
125
+ * @typedef {Object} CheckNonceOfSIWEAddressResponse
126
+ * @property {boolean} valid - if the checking was valid or not
127
+ * @property {string} message - more information about the validity
128
+ * @property {string} token - token to sign into Firebase
129
+ */
130
+ export type CheckNonceOfSIWEAddressResponse = {
131
+ valid: boolean;
132
+ message: string;
133
+ token: string;
134
+ };
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-5fad82d",
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": "32fddd9a2d6199f1b0701cb4b8c3bc418660df31"
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,185 @@
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_complete)
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 Sign In With Ethereum...`, `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_complete)
51
+ } catch (error: any) {
52
+ console.log(
53
+ `${theme.symbols.info} Please authenticate via SIWE at ${OAuthDeviceCode.verification_uri_complete}`
54
+ )
55
+ }
56
+ spinner.stop()
57
+ }
58
+
59
+ /**
60
+ * Return the token to sign in to Firebase after passing the SIWE Device Flow
61
+ * @param clientId <string> - The client id of the Auth0 application.
62
+ * @param firebaseFunctions <any> - The Firebase functions instance to call the cloud function
63
+ * @returns <string> - The token to sign in to Firebase
64
+ */
65
+ const executeSIWEDeviceFlow = async (clientId: string, firebaseFunctions: any): Promise<string> => {
66
+ // Call Auth0 endpoint to request device code uri
67
+ const OAuthDeviceCode = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/device/code`, {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json" },
70
+ body: JSON.stringify({
71
+ client_id: clientId,
72
+ scope: "openid",
73
+ audience: `${process.env.AUTH0_APPLICATION_URL}/api/v2/`
74
+ })
75
+ }).then((_res) => _res.json())) as OAuthDeviceCodeResponse
76
+ if (OAuthDeviceCode.error) {
77
+ showError(OAuthDeviceCode.error_description, true)
78
+ deleteLocalAuthMethod()
79
+ deleteLocalAccessToken()
80
+ }
81
+ await showVerificationCodeAndUri(OAuthDeviceCode)
82
+ // Poll Auth0 endpoint until you get token or request expires
83
+ let isSignedIn = false
84
+ let isExpired = false
85
+ let auth0Token = ""
86
+ while (!isSignedIn && !isExpired) {
87
+ // Call Auth0 endpoint to request token
88
+ const OAuthToken = (await fetch(`${process.env.AUTH0_APPLICATION_URL}/oauth/token`, {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify({
92
+ client_id: clientId,
93
+ device_code: OAuthDeviceCode.device_code,
94
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
95
+ })
96
+ }).then((_res) => _res.json())) as OAuthTokenResponse
97
+ if (OAuthToken.error) {
98
+ if (OAuthToken.error === "authorization_pending") {
99
+ // Wait for the user to sign in
100
+ await sleep(OAuthDeviceCode.interval * 1000)
101
+ } else if (OAuthToken.error === "slow_down") {
102
+ // Wait for the user to sign in
103
+ await sleep(OAuthDeviceCode.interval * 1000 * 2)
104
+ } else if (OAuthToken.error === "expired_token") {
105
+ // The user didn't sign in on time
106
+ isExpired = true
107
+ }
108
+ } else {
109
+ // The user signed in
110
+ isSignedIn = true
111
+ auth0Token = OAuthToken.access_token
112
+ }
113
+ }
114
+ // Send token to cloud function to check nonce, create user and retrieve token
115
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.checkNonceOfSIWEAddress)
116
+ const result = await cf({
117
+ auth0Token
118
+ })
119
+ const { token, valid, message } = result.data as CheckNonceOfSIWEAddressResponse
120
+ if (!valid) {
121
+ showError(message, true)
122
+ deleteLocalAuthMethod()
123
+ deleteLocalAccessToken()
124
+ }
125
+ return token
126
+ }
127
+
128
+ /**
129
+ * Auth command using Sign In With Ethereum
130
+ * @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.
131
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the SIWE documentation.
132
+ */
133
+ const authSIWE = async () => {
134
+ try {
135
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices()
136
+ // Console more context for the user.
137
+ console.log(
138
+ `${theme.symbols.info} ${theme.text.bold(
139
+ `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`
140
+ )}\n`
141
+ )
142
+ const spinner = customSpinner(`Checking authentication token...`, `clock`)
143
+ spinner.start()
144
+ await sleep(5000)
145
+
146
+ // Manage OAuth Github or SIWE token.
147
+ const isLocalTokenStored = checkLocalAccessToken()
148
+
149
+ if (!isLocalTokenStored) {
150
+ spinner.fail(`No local authentication token found\n`)
151
+
152
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
153
+ const newToken = await executeSIWEDeviceFlow(String(process.env.AUTH_SIWE_CLIENT_ID), firebaseFunctions)
154
+
155
+ // Store the new access token.
156
+ setLocalAuthMethod("siwe")
157
+ setLocalAccessToken(newToken)
158
+ } else spinner.succeed(`Local authentication token found\n`)
159
+
160
+ // Get access token from local store.
161
+ const token = String(getLocalAccessToken())
162
+
163
+ spinner.text = `Authenticating...`
164
+ spinner.start()
165
+
166
+ // Exchange token for credential.
167
+ const credentials = await signInWithCustomToken(getAuth(), token)
168
+ spinner.succeed(`Authenticated as ${theme.text.bold(credentials.user.uid)}.`)
169
+
170
+ console.log(
171
+ `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
172
+ `phase2cli logout`
173
+ )} command`
174
+ )
175
+ process.exit(0)
176
+ } catch (error) {
177
+ // Delete local token.
178
+ console.log("An error crashed the process. Deleting local token and identity.")
179
+ console.error(error)
180
+ deleteLocalAuthMethod()
181
+ deleteLocalAccessToken()
182
+ }
183
+ }
184
+
185
+ export default authSIWE
@@ -1,5 +1,6 @@
1
1
  import { collection, doc, getDocs } from "firebase/firestore"
2
- import { commonTerms, getAllCeremonies } from "@devtion/actions"
2
+ import { ParticipantDocument, UserDocument, commonTerms, getAllCeremonies } from "@devtion/actions"
3
+ import theme from "../../lib/theme.js"
3
4
  import { bootstrapCommandExecutionAndServices } from "../../lib/services.js"
4
5
  import { showError } from "../../lib/errors.js"
5
6
  import { promptForCeremonySelection } from "../../lib/prompts.js"
@@ -9,18 +10,43 @@ const listParticipants = async () => {
9
10
  const { firestoreDatabase } = await bootstrapCommandExecutionAndServices()
10
11
 
11
12
  const allCeremonies = await getAllCeremonies(firestoreDatabase)
12
- const selectedCeremony = await promptForCeremonySelection(allCeremonies, true)
13
+ const selectedCeremony = await promptForCeremonySelection(
14
+ allCeremonies,
15
+ true,
16
+ "Which ceremony would you like to see participants?"
17
+ )
13
18
 
14
19
  const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id)
15
20
  const participantsRef = collection(docRef, "participants")
16
21
  const participantsSnapshot = await getDocs(participantsRef)
17
- const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data().userId)
18
- console.log(participants)
22
+ const participants = participantsSnapshot.docs.map(
23
+ (participantDoc) => participantDoc.data() as ParticipantDocument
24
+ )
19
25
 
20
- /* const usersRef = collection(firestoreDatabase, "users")
26
+ const usersRef = collection(firestoreDatabase, "users")
21
27
  const usersSnapshot = await getDocs(usersRef)
22
- const users = usersSnapshot.docs.map((userDoc) => userDoc.data())
23
- console.log(users) */
28
+ const users = usersSnapshot.docs.map((userDoc) => {
29
+ const data = userDoc.data() as UserDocument
30
+ return { id: userDoc.id, ...data }
31
+ })
32
+
33
+ const participantDetails = participants
34
+ .map((participant) => {
35
+ const user = users.find((_user) => _user.id === participant.userId)
36
+ if (!user) return null
37
+ return {
38
+ id: user.name,
39
+ status: participant.status,
40
+ contributionStep: participant.contributionStep,
41
+ lastUpdated: new Date(participant.lastUpdated)
42
+ }
43
+ })
44
+ .filter((user) => user !== null)
45
+
46
+ const participantsDone = participantDetails.filter((participant) => participant.status === "DONE")
47
+ console.log(participantDetails)
48
+ console.log(`${theme.text.underlined("Total participants:")} ${participantDetails.length}`)
49
+ console.log(`${theme.text.underlined("Total participants finished:")} ${participantsDone.length}`)
24
50
  } catch (err: any) {
25
51
  showError(`Something went wrong: ${err.toString()}`, true)
26
52
  }