@iexec/iapp-maker 0.0.1-alpha-nightly-6be476717b877e4857383c375d5f209dbd288f99 → 0.0.1-alpha-nightly-c88f1c62428cda426ca60c8b374fb74970fb6fba

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iexec/iapp-maker",
3
- "version": "0.0.1-alpha-nightly-6be476717b877e4857383c375d5f209dbd288f99",
3
+ "version": "0.0.1-alpha-nightly-c88f1c62428cda426ca60c8b374fb74970fb6fba",
4
4
  "description": "A CLI to guide you through the process of building an iExec iApp",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -44,7 +44,6 @@
44
44
  "magic-bytes.js": "^1.10.0",
45
45
  "ora": "^8.1.1",
46
46
  "prompts": "^2.4.2",
47
- "undici": "^6.21.0",
48
47
  "uuid": "^11.0.3",
49
48
  "yargs": "^17.7.2",
50
49
  "zod": "^3.23.8",
package/src/cmd/deploy.js CHANGED
@@ -6,7 +6,10 @@ import {
6
6
  import { sconify } from '../utils/sconify.js';
7
7
  import { askForDockerhubUsername } from '../cli-helpers/askForDockerhubUsername.js';
8
8
  import { askForWalletAddress } from '../cli-helpers/askForWalletAddress.js';
9
- import { readPackageJonConfig } from '../utils/iAppConfigFile.js';
9
+ import {
10
+ projectNameToImageName,
11
+ readIAppConfig,
12
+ } from '../utils/iAppConfigFile.js';
10
13
  import { askForDockerhubAccessToken } from '../cli-helpers/askForDockerhubAccessToken.js';
11
14
  import { handleCliError } from '../cli-helpers/handleCliError.js';
12
15
  import { getSpinner } from '../cli-helpers/spinner.js';
@@ -22,6 +25,8 @@ export async function deploy() {
22
25
  const spinner = getSpinner();
23
26
  try {
24
27
  await goToProjectRoot({ spinner });
28
+ const { projectName } = await readIAppConfig();
29
+
25
30
  const dockerhubUsername = await askForDockerhubUsername({ spinner });
26
31
  const dockerhubAccessToken = await askForDockerhubAccessToken({ spinner });
27
32
 
@@ -38,6 +43,8 @@ export async function deploy() {
38
43
  throw Error('Invalid version');
39
44
  }
40
45
 
46
+ const imageTag = `${dockerhubUsername}/${projectNameToImageName(projectName)}:${iAppVersion}`;
47
+
41
48
  const appSecret = await askForAppSecret({ spinner });
42
49
 
43
50
  const walletAddress = await askForWalletAddress({ spinner });
@@ -54,11 +61,6 @@ export async function deploy() {
54
61
  iexec = getIExecDebug(privateKey);
55
62
  }
56
63
 
57
- const config = await readPackageJonConfig();
58
- const iAppName = config.name.toLowerCase();
59
-
60
- const imageTag = `${dockerhubUsername}/${iAppName}:${iAppVersion}`;
61
-
62
64
  // just start the spinner, no need to persist success in terminal
63
65
  spinner.start('Checking docker daemon is running...');
64
66
  await checkDockerDaemon();
@@ -89,7 +91,6 @@ export async function deploy() {
89
91
  'Transforming your image into a TEE image and deploying on iExec, this may take a few minutes...'
90
92
  );
91
93
  const { sconifiedImage, appContractAddress } = await sconify({
92
- sconifyForProd: false,
93
94
  iAppNameToSconify: imageTag,
94
95
  walletAddress,
95
96
  dockerhubAccessToken,
package/src/cmd/init.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import figlet from 'figlet';
3
3
  import { mkdir } from 'node:fs/promises';
4
+ import { fromError } from 'zod-validation-error';
4
5
  import { folderExists } from '../utils/fs.utils.js';
5
6
  import { initIAppWorkspace } from '../utils/initIAppWorkspace.js';
6
7
  import { getSpinner } from '../cli-helpers/spinner.js';
@@ -8,6 +9,7 @@ import { handleCliError } from '../cli-helpers/handleCliError.js';
8
9
  import { generateWallet } from '../utils/generateWallet.js';
9
10
  import * as color from '../cli-helpers/color.js';
10
11
  import { hintBox } from '../cli-helpers/box.js';
12
+ import { projectNameSchema } from '../utils/iAppConfigFile.js';
11
13
 
12
14
  const targetDir = 'hello-world';
13
15
 
@@ -30,6 +32,16 @@ export async function init() {
30
32
  name: 'projectName',
31
33
  message: `What's your project name? ${color.promptHelper('(A folder with this name will be created)')}`,
32
34
  initial: targetDir,
35
+ validate: (value) => {
36
+ try {
37
+ projectNameSchema.parse(value);
38
+ return true;
39
+ } catch (e) {
40
+ return fromError(e)
41
+ .details.map((issue) => issue.message)
42
+ .join('; ');
43
+ }
44
+ },
33
45
  });
34
46
 
35
47
  if (await folderExists(projectName)) {
package/src/cmd/run.js CHANGED
@@ -133,8 +133,7 @@ export async function runInDebug({
133
133
  tag: SCONE_TAG,
134
134
  });
135
135
  const apporder = await iexec.order.signApporder(apporderTemplate);
136
- await iexec.order.publishApporder(apporder);
137
- spinner.succeed('AppOrder created and published');
136
+ spinner.succeed('AppOrder created');
138
137
 
139
138
  // Dataset Order
140
139
  let datasetorder;
@@ -29,6 +29,17 @@ export async function getAuthToken({
29
29
  `Fail to get authorization token for scope=${repository}:${action}`
30
30
  );
31
31
  }
32
- const { token } = await response.json();
33
- return token;
32
+ return response
33
+ .json()
34
+ .catch(() => {
35
+ throw Error(`Unexpected response from dockerhub auth server`);
36
+ })
37
+ .then(({ token }) => {
38
+ if (!token) {
39
+ throw Error(
40
+ `Unexpected response from dockerhub auth server: Missing token`
41
+ );
42
+ }
43
+ return token;
44
+ });
34
45
  }
@@ -3,8 +3,27 @@ import { z } from 'zod';
3
3
  import { fromError } from 'zod-validation-error';
4
4
  import { CONFIG_FILE } from '../config/config.js';
5
5
 
6
+ export const projectNameSchema = z
7
+ .string()
8
+ .min(2, 'Must contain at least 2 characters') // docker image name constraint
9
+ .refine(
10
+ (value) => !/(^\s)|(\s$)/.test(value),
11
+ 'Should not start or end with space'
12
+ )
13
+ .refine(
14
+ (value) => /^[a-zA-Z0-9- ]+$/.test(value ?? ''),
15
+ 'Only letters, numbers, spaces, and hyphens are allowed'
16
+ );
17
+
18
+ const dockerImageNameSchema = z
19
+ .string()
20
+ .refine(
21
+ (value) => /^[a-z0-9-]+$/.test(value ?? ''),
22
+ 'Invalid docker image name'
23
+ );
24
+
6
25
  const jsonConfigFileSchema = z.object({
7
- projectName: z.string(),
26
+ projectName: projectNameSchema,
8
27
  dockerhubUsername: z.string().optional(),
9
28
  dockerhubAccessToken: z.string().optional(),
10
29
  walletAddress: z.string().optional(),
@@ -12,6 +31,12 @@ const jsonConfigFileSchema = z.object({
12
31
  appSecret: z.string().optional().nullable(), // can be null or string (null means do no use secret)
13
32
  });
14
33
 
34
+ // transform the projectName into a suitable docker image name (no space, lowercase only)
35
+ export function projectNameToImageName(projectName = '') {
36
+ const imageName = projectName.toLowerCase().replaceAll(' ', '-');
37
+ return dockerImageNameSchema.parse(imageName);
38
+ }
39
+
15
40
  // Read JSON configuration file
16
41
  export async function readIAppConfig() {
17
42
  const configContent = await readFile(CONFIG_FILE, 'utf8').catch(() => {
@@ -37,16 +62,6 @@ export async function readIAppConfig() {
37
62
  }
38
63
  }
39
64
 
40
- // Read package.json file
41
- export async function readPackageJonConfig() {
42
- try {
43
- const packageContent = await readFile('./package.json', 'utf8');
44
- return JSON.parse(packageContent);
45
- } catch {
46
- throw Error('Failed to read `package.json` file.');
47
- }
48
- }
49
-
50
65
  // Utility function to write the iApp JSON configuration file
51
66
  export async function writeIAppConfig(config) {
52
67
  await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
@@ -1,6 +1,5 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { request } from 'undici';
4
3
  import { TEST_INPUT_DIR } from '../config/config.js';
5
4
 
6
5
  // TODO we may want to cache to avoid downloading large input files over and over
@@ -19,9 +18,7 @@ export async function prepareInputFile(url) {
19
18
  if (name === '.' || name === '..') {
20
19
  throw Error('Invalid computed file name');
21
20
  }
22
- await request(url, {
23
- throwOnError: true,
24
- }).then(async (response) => {
21
+ await fetch(url).then(async (response) => {
25
22
  await mkdir(TEST_INPUT_DIR, { recursive: true }); // ensure input dir
26
23
  await writeFile(join(TEST_INPUT_DIR, name), response.body);
27
24
  });
@@ -1,19 +1,19 @@
1
- import { request } from 'undici';
2
1
  import { addDeploymentData } from './cacheExecutions.js';
3
2
  import { SCONIFY_API_URL } from '../config/config.js';
4
3
  import { getAuthToken } from '../utils/dockerhub.js';
4
+ import { sleep } from './sleep.js';
5
+
6
+ const INITIAL_RETRY_PERIOD = 20 * 1000; // 20s
7
+
8
+ class TooManyRequestsError extends Error {}
5
9
 
6
10
  export async function sconify({
7
- sconifyForProd,
8
11
  iAppNameToSconify,
9
12
  walletAddress,
10
13
  dockerhubAccessToken,
11
14
  dockerhubUsername,
15
+ tryCount = 0,
12
16
  }) {
13
- if (sconifyForProd) {
14
- throw Error('This feature is not yet implemented. Coming soon ...');
15
- }
16
-
17
17
  let appContractAddress;
18
18
  let sconifiedImage;
19
19
  try {
@@ -26,7 +26,7 @@ export async function sconify({
26
26
  dockerhubUsername,
27
27
  });
28
28
 
29
- const { body } = await request(`${SCONIFY_API_URL}/sconify`, {
29
+ const jsonResponse = await fetch(`${SCONIFY_API_URL}/sconify`, {
30
30
  method: 'POST',
31
31
  headers: {
32
32
  'Content-Type': 'application/json',
@@ -37,26 +37,56 @@ export async function sconify({
37
37
  dockerhubPushToken: pushToken, // used for pushing sconified image on user repo
38
38
  yourWalletPublicAddress: walletAddress,
39
39
  }),
40
- throwOnError: true,
41
- });
40
+ })
41
+ .catch(() => {
42
+ throw Error("Can't reach TEE transformation server!");
43
+ })
44
+ .then((res) => {
45
+ if (res.ok) {
46
+ return res.json().catch(() => {
47
+ // failed to parse body
48
+ throw Error('Unexpected server response');
49
+ });
50
+ }
51
+ if (res.status === 429) {
52
+ throw new TooManyRequestsError(
53
+ 'TEE transformation server is busy, retry later'
54
+ );
55
+ }
56
+ // try getting error message from json body
57
+ return res
58
+ .json()
59
+ .catch(() => {
60
+ // failed to parse body
61
+ throw Error('Unknown server error');
62
+ })
63
+ .then(({ error }) => {
64
+ throw Error(error || 'Unknown server error');
65
+ });
66
+ });
42
67
 
43
68
  // Extract necessary information
44
- const json = await body.json();
45
- sconifiedImage = json.sconifiedImage;
46
- appContractAddress = json.appContractAddress;
69
+ if (!jsonResponse.appContractAddress) {
70
+ throw Error('Unexpected server response: missing appContractAddress');
71
+ }
72
+ if (!jsonResponse.sconifiedImage) {
73
+ throw Error('Unexpected server response: missing sconifiedImage');
74
+ }
75
+ appContractAddress = jsonResponse.appContractAddress;
76
+ sconifiedImage = jsonResponse.sconifiedImage;
47
77
  } catch (err) {
48
- let reason;
49
- if (err.body) {
50
- reason = err.body;
51
- } else if (
52
- err?.code === 'ECONNREFUSED' ||
53
- err?.code === 'UND_ERR_CONNECT_TIMEOUT'
54
- ) {
55
- reason = "Can't reach TEE transformation server!";
56
- } else {
57
- reason = err.toString();
78
+ // retry with exponential backoff
79
+ if (err instanceof TooManyRequestsError && tryCount < 3) {
80
+ await sleep(INITIAL_RETRY_PERIOD * Math.pow(2, tryCount));
81
+ return sconify({
82
+ iAppNameToSconify,
83
+ walletAddress,
84
+ dockerhubAccessToken,
85
+ dockerhubUsername,
86
+ tryCount: tryCount + 1,
87
+ });
58
88
  }
59
- throw Error(`Failed to transform your app into a TEE app: ${reason}`);
89
+ throw Error(`Failed to transform your app into a TEE app: ${err.message}`);
60
90
  }
61
91
 
62
92
  // Add deployment data to deployments.json
@@ -0,0 +1 @@
1
+ export const sleep = async (ms) => new Promise((res) => setTimeout(res, ms));
@@ -1,13 +1,13 @@
1
1
  {
2
- "name": "hello-world",
3
- "version": "1.0.0",
2
+ "name": "iexec-hello-world-iapp",
3
+ "version": "0.0.1",
4
4
  "lockfileVersion": 1,
5
5
  "requires": true,
6
6
  "dependencies": {
7
7
  "@iexec/dataprotector-deserializer": {
8
- "version": "0.1.0",
9
- "resolved": "https://registry.npmjs.org/@iexec/dataprotector-deserializer/-/dataprotector-deserializer-0.1.0.tgz",
10
- "integrity": "sha512-qjrcWR286qegASxcHMSZCmt6dKubIHXIqceXqSscq4vzEDHn4nIwHUabDbTLQbvdrF0XGswNeQuYc10vgjZtow==",
8
+ "version": "0.1.1",
9
+ "resolved": "https://registry.npmjs.org/@iexec/dataprotector-deserializer/-/dataprotector-deserializer-0.1.1.tgz",
10
+ "integrity": "sha512-CSz1JWnslm2X3gjL1cx/qqovnmvJSFWDyJMw0ZGvqnYnNatgIqHn+Aky2iO4K0HsArfqmgV3ySIpdPfu/N2M0w==",
11
11
  "requires": {
12
12
  "borsh": "^2.0.0",
13
13
  "jszip": "^3.10.1"
@@ -8,7 +8,7 @@
8
8
  "npm": "<7.0.0"
9
9
  },
10
10
  "dependencies": {
11
- "@iexec/dataprotector-deserializer": "^0.1.0",
11
+ "@iexec/dataprotector-deserializer": "^0.1.1",
12
12
  "figlet": "^1.7.0"
13
13
  }
14
14
  }