@devtion/devcli 0.0.0-004e6ad
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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/.env +55 -0
- package/dist/index.js +3635 -0
- package/dist/public/mini-semaphore.wasm +0 -0
- package/dist/public/mini-semaphore.zkey +0 -0
- package/dist/types/commands/auth.d.ts +25 -0
- package/dist/types/commands/authBandada.d.ts +2 -0
- package/dist/types/commands/authSIWE.d.ts +7 -0
- package/dist/types/commands/ceremony/index.d.ts +3 -0
- package/dist/types/commands/ceremony/listParticipants.d.ts +2 -0
- package/dist/types/commands/clean.d.ts +6 -0
- package/dist/types/commands/contribute.d.ts +139 -0
- package/dist/types/commands/finalize.d.ts +52 -0
- package/dist/types/commands/index.d.ts +11 -0
- package/dist/types/commands/listCeremonies.d.ts +5 -0
- package/dist/types/commands/logout.d.ts +6 -0
- package/dist/types/commands/observe.d.ts +22 -0
- package/dist/types/commands/setup.d.ts +86 -0
- package/dist/types/commands/validate.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/lib/bandada.d.ts +6 -0
- package/dist/types/lib/errors.d.ts +60 -0
- package/dist/types/lib/files.d.ts +65 -0
- package/dist/types/lib/localConfigs.d.ts +148 -0
- package/dist/types/lib/prompts.d.ts +104 -0
- package/dist/types/lib/services.d.ts +31 -0
- package/dist/types/lib/theme.d.ts +42 -0
- package/dist/types/lib/utils.d.ts +159 -0
- package/dist/types/types/index.d.ts +134 -0
- package/package.json +108 -0
- package/src/commands/auth.ts +214 -0
- package/src/commands/authBandada.ts +120 -0
- package/src/commands/authSIWE.ts +185 -0
- package/src/commands/ceremony/index.ts +20 -0
- package/src/commands/ceremony/listParticipants.ts +56 -0
- package/src/commands/clean.ts +49 -0
- package/src/commands/contribute.ts +1116 -0
- package/src/commands/finalize.ts +395 -0
- package/src/commands/index.ts +11 -0
- package/src/commands/listCeremonies.ts +31 -0
- package/src/commands/logout.ts +69 -0
- package/src/commands/observe.ts +197 -0
- package/src/commands/setup.ts +912 -0
- package/src/commands/validate.ts +28 -0
- package/src/index.ts +88 -0
- package/src/lib/bandada.ts +51 -0
- package/src/lib/errors.ts +77 -0
- package/src/lib/files.ts +102 -0
- package/src/lib/localConfigs.ts +240 -0
- package/src/lib/prompts.ts +745 -0
- package/src/lib/services.ts +214 -0
- package/src/lib/theme.ts +45 -0
- package/src/lib/utils.ts +813 -0
- package/src/types/conf.d.ts +16 -0
- package/src/types/index.ts +145 -0
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devtion/devcli",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0-004e6ad",
|
|
5
|
+
"description": "All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies",
|
|
6
|
+
"repository": "git@github.com:privacy-scaling-explorations/p0tion.git",
|
|
7
|
+
"homepage": "https://github.com/privacy-scaling-explorations/p0tion",
|
|
8
|
+
"bugs": "https://github.com/privacy-scaling-explorations/p0tion/issues",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"bin": "dist/index.js",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/types/index.d.ts",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=16.14.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/",
|
|
18
|
+
"src/",
|
|
19
|
+
"types/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"typescript",
|
|
24
|
+
"zero-knowledge",
|
|
25
|
+
"zk-snarks",
|
|
26
|
+
"phase-2",
|
|
27
|
+
"trusted-setup",
|
|
28
|
+
"ceremony",
|
|
29
|
+
"snarkjs",
|
|
30
|
+
"circom"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript",
|
|
34
|
+
"build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript",
|
|
35
|
+
"start": "ts-node --esm ./src/index.ts",
|
|
36
|
+
"auth": "yarn start auth",
|
|
37
|
+
"auth:siwe": "yarn start auth-siwe",
|
|
38
|
+
"auth:bandada": "yarn start auth-bandada",
|
|
39
|
+
"contribute": "yarn start contribute",
|
|
40
|
+
"clean": "yarn start clean",
|
|
41
|
+
"list": "yarn start list",
|
|
42
|
+
"logout": "yarn start logout",
|
|
43
|
+
"validate": "yarn start validate",
|
|
44
|
+
"ceremony:participants": "yarn start ceremony participants",
|
|
45
|
+
"coordinate:setup": "yarn start coordinate setup",
|
|
46
|
+
"coordinate:observe": "yarn start coordinate observe",
|
|
47
|
+
"coordinate:finalize": "yarn start coordinate finalize",
|
|
48
|
+
"docs": "typedoc src/**/*.ts --out ../../docs/phase2cli"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/clear": "^0.1.2",
|
|
52
|
+
"@types/cli-progress": "^3.11.0",
|
|
53
|
+
"@types/figlet": "^1.5.6",
|
|
54
|
+
"@types/mime-types": "^2.1.1",
|
|
55
|
+
"@types/node-emoji": "^1.8.2",
|
|
56
|
+
"@types/node-fetch": "^2.6.3",
|
|
57
|
+
"@types/ora": "^3.2.0",
|
|
58
|
+
"@types/prompts": "^2.4.4",
|
|
59
|
+
"@types/rollup-plugin-auto-external": "^2.0.2",
|
|
60
|
+
"@types/winston": "^2.4.4",
|
|
61
|
+
"rollup-plugin-auto-external": "^2.0.0",
|
|
62
|
+
"rollup-plugin-cleanup": "^3.2.1",
|
|
63
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
64
|
+
"rollup-plugin-typescript2": "^0.34.1",
|
|
65
|
+
"solc": "^0.8.19",
|
|
66
|
+
"ts-node": "^10.9.1",
|
|
67
|
+
"typescript": "^5.0.4"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@adobe/node-fetch-retry": "^2.2.0",
|
|
71
|
+
"@aws-sdk/client-s3": "^3.329.0",
|
|
72
|
+
"@bandada/api-sdk": "^1.0.0-beta.1",
|
|
73
|
+
"@devtion/actions": "latest",
|
|
74
|
+
"@octokit/auth-oauth-app": "^5.0.5",
|
|
75
|
+
"@octokit/auth-oauth-device": "^4.0.4",
|
|
76
|
+
"@octokit/request": "^6.2.3",
|
|
77
|
+
"@semaphore-protocol/identity": "^3.15.1",
|
|
78
|
+
"blakejs": "^1.2.1",
|
|
79
|
+
"boxen": "^7.1.0",
|
|
80
|
+
"chalk": "^5.2.0",
|
|
81
|
+
"clear": "^0.1.0",
|
|
82
|
+
"cli-progress": "^3.12.0",
|
|
83
|
+
"clipboardy": "^3.0.0",
|
|
84
|
+
"commander": "^10.0.1",
|
|
85
|
+
"conf": "^11.0.1",
|
|
86
|
+
"dotenv": "^16.0.3",
|
|
87
|
+
"ethers": "^6.9.0",
|
|
88
|
+
"figlet": "^1.6.0",
|
|
89
|
+
"firebase": "^9.21.0",
|
|
90
|
+
"log-symbols": "^5.1.0",
|
|
91
|
+
"mime-types": "^2.1.35",
|
|
92
|
+
"node-disk-info": "^1.3.0",
|
|
93
|
+
"node-emoji": "^1.11.0",
|
|
94
|
+
"node-fetch": "^3.3.1",
|
|
95
|
+
"open": "^9.1.0",
|
|
96
|
+
"ora": "^6.3.0",
|
|
97
|
+
"prompts": "^2.4.2",
|
|
98
|
+
"rimraf": "^5.0.0",
|
|
99
|
+
"rollup": "^3.21.6",
|
|
100
|
+
"snarkjs": "0.7.3",
|
|
101
|
+
"timer-node": "^5.0.7",
|
|
102
|
+
"winston": "^3.8.2"
|
|
103
|
+
},
|
|
104
|
+
"publishConfig": {
|
|
105
|
+
"access": "public"
|
|
106
|
+
},
|
|
107
|
+
"gitHead": "8f3da5596cd864cdd387a32c05622efeaf0c10f7"
|
|
108
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device"
|
|
3
|
+
import { Verification } from "@octokit/auth-oauth-device/dist-types/types.js"
|
|
4
|
+
import clipboard from "clipboardy"
|
|
5
|
+
import dotenv from "dotenv"
|
|
6
|
+
import open from "open"
|
|
7
|
+
import figlet from "figlet"
|
|
8
|
+
import { fileURLToPath } from "url"
|
|
9
|
+
import { dirname } from "path"
|
|
10
|
+
import { GENERIC_ERRORS, showError } from "../lib/errors.js"
|
|
11
|
+
import {
|
|
12
|
+
checkLocalAccessToken,
|
|
13
|
+
getLocalAccessToken,
|
|
14
|
+
setLocalAccessToken,
|
|
15
|
+
setLocalAuthMethod
|
|
16
|
+
} from "../lib/localConfigs.js"
|
|
17
|
+
import { bootstrapCommandExecutionAndServices, signInToFirebase } from "../lib/services.js"
|
|
18
|
+
import theme from "../lib/theme.js"
|
|
19
|
+
import {
|
|
20
|
+
customSpinner,
|
|
21
|
+
exchangeGithubTokenForCredentials,
|
|
22
|
+
getGithubProviderUserId,
|
|
23
|
+
getUserHandleFromProviderUserId,
|
|
24
|
+
sleep,
|
|
25
|
+
terminate
|
|
26
|
+
} from "../lib/utils.js"
|
|
27
|
+
|
|
28
|
+
const packagePath = `${dirname(fileURLToPath(import.meta.url))}`
|
|
29
|
+
dotenv.config({
|
|
30
|
+
path: packagePath.includes(`src/lib`)
|
|
31
|
+
? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
|
|
32
|
+
: `${dirname(fileURLToPath(import.meta.url))}/.env`
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Custom countdown which throws an error when expires.
|
|
37
|
+
* @param expirationInSeconds <number> - the expiration time in seconds.
|
|
38
|
+
*/
|
|
39
|
+
export const expirationCountdownForGithubOAuth = (expirationInSeconds: number) => {
|
|
40
|
+
// Prepare data.
|
|
41
|
+
let secondsCounter = expirationInSeconds <= 60 ? expirationInSeconds : 60
|
|
42
|
+
const interval = 1 // 1s.
|
|
43
|
+
|
|
44
|
+
setInterval(() => {
|
|
45
|
+
if (expirationInSeconds !== 0) {
|
|
46
|
+
// Update time and seconds counter.
|
|
47
|
+
expirationInSeconds -= interval
|
|
48
|
+
secondsCounter -= interval
|
|
49
|
+
|
|
50
|
+
if (secondsCounter % 60 === 0) secondsCounter = 0
|
|
51
|
+
|
|
52
|
+
// Notify user.
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
`${theme.symbols.warning} Expires in ${theme.text.bold(
|
|
55
|
+
theme.colors.magenta(`00:${Math.floor(expirationInSeconds / 60)}:${secondsCounter}`)
|
|
56
|
+
)}\r`
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
process.stdout.write(`\n\n`) // workaround to \r.
|
|
60
|
+
showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRATION, true)
|
|
61
|
+
}
|
|
62
|
+
}, interval * 1000) // ms.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Callback to manage the data requested for Github OAuth2.0 device flow.
|
|
67
|
+
* @param verification <Verification> - the data from Github OAuth2.0 device flow.
|
|
68
|
+
*/
|
|
69
|
+
export const onVerification = async (verification: Verification): Promise<void> => {
|
|
70
|
+
// Copy code to clipboard.
|
|
71
|
+
let noClipboard = false
|
|
72
|
+
try {
|
|
73
|
+
clipboard.writeSync(verification.user_code)
|
|
74
|
+
clipboard.readSync()
|
|
75
|
+
} catch (error) {
|
|
76
|
+
noClipboard = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Display data.
|
|
80
|
+
console.log(
|
|
81
|
+
`${theme.symbols.warning} Visit ${theme.text.bold(
|
|
82
|
+
theme.text.underlined(verification.verification_uri)
|
|
83
|
+
)} on this device to generate a new token and authenticate\n`
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.log(theme.colors.magenta(figlet.textSync("Code is Below", { font: "ANSI Shadow" })), "\n")
|
|
87
|
+
|
|
88
|
+
const message = !noClipboard ? `has been copied to your clipboard (${theme.emojis.clipboard})` : ``
|
|
89
|
+
console.log(
|
|
90
|
+
`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} ${message} ${
|
|
91
|
+
theme.symbols.success
|
|
92
|
+
}\n`
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const spinner = customSpinner(`Redirecting to Github...`, `clock`)
|
|
96
|
+
spinner.start()
|
|
97
|
+
|
|
98
|
+
await sleep(10000) // ~10s to make users able to read the CLI.
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Automatically open the page (# Step 2).
|
|
102
|
+
await open(verification.verification_uri)
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
console.log(`${theme.symbols.info} Please authenticate via GitHub at ${verification.verification_uri}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
spinner.stop()
|
|
108
|
+
|
|
109
|
+
// Countdown for time expiration.
|
|
110
|
+
expirationCountdownForGithubOAuth(verification.expires_in)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return the Github OAuth 2.0 token using manual Device Flow authentication process.
|
|
115
|
+
* @param clientId <string> - the client id for the CLI OAuth app.
|
|
116
|
+
* @returns <string> the Github OAuth 2.0 token.
|
|
117
|
+
*/
|
|
118
|
+
export const executeGithubDeviceFlow = async (clientId: string): Promise<string> => {
|
|
119
|
+
/**
|
|
120
|
+
* Github OAuth 2.0 Device Flow.
|
|
121
|
+
* # Step 1: Request device and user verification codes and gets auth verification uri.
|
|
122
|
+
* # Step 2: The app prompts the user to enter a user verification code at https://github.com/login/device.
|
|
123
|
+
* # Step 3: The app polls/asks for the user authentication status.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
const clientType = "oauth-app"
|
|
127
|
+
const tokenType = "oauth"
|
|
128
|
+
|
|
129
|
+
// # Step 1.
|
|
130
|
+
const auth = createOAuthDeviceAuth({
|
|
131
|
+
clientType,
|
|
132
|
+
clientId,
|
|
133
|
+
scopes: ["gist"],
|
|
134
|
+
onVerification
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// # Step 3.
|
|
138
|
+
const { token } = await auth({
|
|
139
|
+
type: tokenType
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return token
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Auth command.
|
|
147
|
+
* @notice The auth command allows a user to make the association of their Github account with the CLI by leveraging OAuth 2.0 as an authentication mechanism.
|
|
148
|
+
* @dev Under the hood, the command handles a manual Device Flow following the guidelines in the Github documentation.
|
|
149
|
+
*/
|
|
150
|
+
const auth = async () => {
|
|
151
|
+
const { firebaseApp } = await bootstrapCommandExecutionAndServices()
|
|
152
|
+
|
|
153
|
+
// Console more context for the user.
|
|
154
|
+
console.log(
|
|
155
|
+
`${theme.symbols.info} ${theme.text.bold(
|
|
156
|
+
`You are about to authenticate on this CLI using your Github account (device flow - OAuth 2.0 mechanism).\n${
|
|
157
|
+
theme.symbols.warning
|
|
158
|
+
} Please, note that only read and write permission for ${theme.text.italic(
|
|
159
|
+
`gists`
|
|
160
|
+
)} will be required in order to publish your contribution transcript!`
|
|
161
|
+
)}\n`
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const spinner = customSpinner(`Checking authentication token...`, `clock`)
|
|
165
|
+
spinner.start()
|
|
166
|
+
|
|
167
|
+
await sleep(5000)
|
|
168
|
+
|
|
169
|
+
// Manage OAuth Github token.
|
|
170
|
+
const isLocalTokenStored = checkLocalAccessToken()
|
|
171
|
+
|
|
172
|
+
if (!isLocalTokenStored) {
|
|
173
|
+
spinner.fail(`No local authentication token found\n`)
|
|
174
|
+
|
|
175
|
+
// Generate a new access token using Github Device Flow (OAuth 2.0).
|
|
176
|
+
const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID))
|
|
177
|
+
|
|
178
|
+
// Store the new access token.
|
|
179
|
+
setLocalAuthMethod("github")
|
|
180
|
+
setLocalAccessToken(newToken)
|
|
181
|
+
} else spinner.succeed(`Local authentication token found\n`)
|
|
182
|
+
|
|
183
|
+
// Get access token from local store.
|
|
184
|
+
const token = getLocalAccessToken()
|
|
185
|
+
|
|
186
|
+
// Exchange token for credential.
|
|
187
|
+
const credentials = exchangeGithubTokenForCredentials(String(token))
|
|
188
|
+
|
|
189
|
+
spinner.text = `Authenticating...`
|
|
190
|
+
spinner.start()
|
|
191
|
+
|
|
192
|
+
// Sign-in to Firebase using credentials.
|
|
193
|
+
await signInToFirebase(firebaseApp, credentials)
|
|
194
|
+
|
|
195
|
+
// Get Github handle.
|
|
196
|
+
const providerUserId = await getGithubProviderUserId(String(token))
|
|
197
|
+
|
|
198
|
+
spinner.succeed(
|
|
199
|
+
`You are authenticated as ${theme.text.bold(
|
|
200
|
+
`@${getUserHandleFromProviderUserId(providerUserId)}`
|
|
201
|
+
)} and now able to interact with zk-SNARK Phase2 Trusted Setup ceremonies`
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// Console more context for the user.
|
|
205
|
+
console.log(
|
|
206
|
+
`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
|
|
207
|
+
`phase2cli logout`
|
|
208
|
+
)} command`
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
terminate(providerUserId)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default auth
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { getAuth, signInWithCustomToken } from "firebase/auth"
|
|
7
|
+
import prompts from "prompts"
|
|
8
|
+
import { getLocalDirname } from "../lib/files.js"
|
|
9
|
+
import theme from "../lib/theme.js"
|
|
10
|
+
import { customSpinner } from "../lib/utils.js"
|
|
11
|
+
import { VerifiedBandadaResponse } from "../types/index.js"
|
|
12
|
+
import { showError } from "../lib/errors.js"
|
|
13
|
+
import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
|
|
14
|
+
import { addMemberToGroup, isGroupMember } from "../lib/bandada.js"
|
|
15
|
+
import {
|
|
16
|
+
checkLocalBandadaIdentity,
|
|
17
|
+
deleteLocalAccessToken,
|
|
18
|
+
deleteLocalAuthMethod,
|
|
19
|
+
deleteLocalBandadaIdentity,
|
|
20
|
+
getLocalBandadaIdentity,
|
|
21
|
+
setLocalAccessToken,
|
|
22
|
+
setLocalAuthMethod,
|
|
23
|
+
setLocalBandadaIdentity
|
|
24
|
+
} from "../lib/localConfigs.js"
|
|
25
|
+
|
|
26
|
+
const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env
|
|
27
|
+
|
|
28
|
+
const authBandada = async () => {
|
|
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)
|
|
53
|
+
|
|
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
|
+
}
|
|
60
|
+
|
|
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
|
|
68
|
+
|
|
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
|
+
}
|
|
116
|
+
|
|
117
|
+
process.exit(0)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default authBandada
|
|
@@ -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
|
|
@@ -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
|