Skip to content

Commit ec48f64

Browse files
fix!: ensure iapp exit 0 and update outputs
- App always create result files - upon success `result.json` contains `"success": true` (rather than `"status": 200`) - upon failure `result.json` contains `"success": false` and the error message in `"error"`
1 parent 4b0515b commit ec48f64

File tree

8 files changed

+700
-526
lines changed

8 files changed

+700
-526
lines changed

dapp/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.env
22
node_modules
33
coverage
4+
tests/_test_outputs_

dapp/src/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const start = require('./sendEmail');
1+
const start = require('./executeTask');
22

33
start().catch((error) => {
44
console.error(`Error: ${error.message}`);

dapp/src/checkEmailPreviousValidation.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
const { request, gql } = require('graphql-request');
22

3+
// TODO: the method could return true if valid, false if invalid, null if no prior validation or unable to check
34
async function checkEmailPreviousValidation({
45
datasetAddress,
56
dappAddresses,
67
pocoSubgraphUrl,
78
}) {
9+
// TODO: add check in bulk task
810
const query = gql`
911
query checkSuccessfulTaskQuery($apps: [String!], $dataset: String!) {
1012
tasks(

dapp/src/emailService.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,5 @@ async function sendEmail({
4040
.catch(() => {
4141
throw new Error('Failed to send email');
4242
});
43-
return {
44-
message: 'Your email has been sent successfully.',
45-
status: 200,
46-
};
4743
}
4844
module.exports = sendEmail;

dapp/src/executeTask.js

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
const { promises: fs } = require('fs');
2+
const {
3+
IExecDataProtectorDeserializer,
4+
} = require('@iexec/dataprotector-deserializer');
5+
const sendEmail = require('./emailService');
6+
const {
7+
validateWorkerEnv,
8+
validateAppSecret,
9+
validateRequesterSecret,
10+
validateProtectedData,
11+
} = require('./validation');
12+
const {
13+
downloadEncryptedContent,
14+
decryptContent,
15+
} = require('./decryptEmailContent');
16+
const { validateEmailAddress } = require('./validateEmailAddress');
17+
const {
18+
checkEmailPreviousValidation,
19+
} = require('./checkEmailPreviousValidation');
20+
21+
async function processProtectedData({
22+
index,
23+
IEXEC_IN,
24+
appDeveloperSecret,
25+
requesterSecret,
26+
}) {
27+
const datasetFilename =
28+
index > 0
29+
? process.env[`IEXEC_DATASET_${index}_FILENAME`]
30+
: process.env.IEXEC_DATASET_FILENAME;
31+
const result = { index, protectedData: datasetFilename };
32+
try {
33+
let protectedData;
34+
try {
35+
const deserializerConfig = datasetFilename
36+
? { protectedDataPath: `${IEXEC_IN}/${datasetFilename}` }
37+
: {};
38+
const deserializer = new IExecDataProtectorDeserializer(
39+
deserializerConfig
40+
);
41+
protectedData = {
42+
email: await deserializer.getValue('email', 'string'),
43+
};
44+
} catch (e) {
45+
throw Error(`Failed to parse ProtectedData ${index}: ${e.message}`);
46+
}
47+
48+
// Validate the protected data
49+
validateProtectedData(protectedData);
50+
51+
// Step 1: Check if email was already validated
52+
let isEmailValidated = await checkEmailPreviousValidation({
53+
datasetAddress: protectedData.email,
54+
dappAddresses: appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS,
55+
pocoSubgraphUrl: appDeveloperSecret.POCO_SUBGRAPH_URL,
56+
});
57+
58+
// Step 2: If not, try Mailgun
59+
if (!isEmailValidated) {
60+
console.log('No prior verification found. Trying Mailgun...');
61+
const mailgunResult = await validateEmailAddress({
62+
emailAddress: protectedData.email,
63+
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
64+
});
65+
66+
if (mailgunResult === true) {
67+
isEmailValidated = true;
68+
result.isEmailValid = true;
69+
} else if (mailgunResult === false) {
70+
result.isEmailValid = false;
71+
throw Error('The protected email address seems to be invalid.');
72+
} else {
73+
console.warn(
74+
'Mailgun verification failed or was unreachable. Proceeding without check.'
75+
);
76+
}
77+
} else {
78+
result.isEmailValid = true;
79+
console.log('Email already verified, skipping Mailgun check.');
80+
}
81+
82+
// Step 3: Decrypt email content
83+
const encryptedEmailContent = await downloadEncryptedContent(
84+
requesterSecret.emailContentMultiAddr
85+
);
86+
const requesterEmailContent = decryptContent(
87+
encryptedEmailContent,
88+
requesterSecret.emailContentEncryptionKey
89+
);
90+
91+
// Step 4: Send email
92+
await sendEmail({
93+
email: protectedData.email,
94+
mailJetApiKeyPublic: appDeveloperSecret.MJ_APIKEY_PUBLIC,
95+
mailJetApiKeyPrivate: appDeveloperSecret.MJ_APIKEY_PRIVATE,
96+
mailJetSender: appDeveloperSecret.MJ_SENDER,
97+
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
98+
emailContent: requesterEmailContent,
99+
emailSubject: requesterSecret.emailSubject,
100+
contentType: requesterSecret.contentType,
101+
senderName: requesterSecret.senderName,
102+
});
103+
result.success = true;
104+
} catch (e) {
105+
result.success = false;
106+
result.error = e.message;
107+
}
108+
console.log(`Protected data ${index} processed:`, result);
109+
return result;
110+
}
111+
112+
async function start() {
113+
const {
114+
IEXEC_IN,
115+
IEXEC_OUT,
116+
IEXEC_APP_DEVELOPER_SECRET,
117+
IEXEC_REQUESTER_SECRET_1,
118+
IEXEC_BULK_SLICE_SIZE,
119+
} = process.env;
120+
121+
// Check worker env
122+
const workerEnv = validateWorkerEnv({ IEXEC_OUT });
123+
124+
let result; // { success: boolean, error?: string, protectedData?: string, results?: { index: number, protectedData: string, success: boolean, error?: string }[] }
125+
let callbackData;
126+
try {
127+
// Parse the app developer secret environment variable
128+
let appDeveloperSecret;
129+
try {
130+
appDeveloperSecret = JSON.parse(IEXEC_APP_DEVELOPER_SECRET);
131+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS =
132+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS
133+
? JSON.parse(appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS)
134+
: undefined;
135+
} catch {
136+
throw Error('Failed to parse the developer secret');
137+
}
138+
appDeveloperSecret = validateAppSecret(appDeveloperSecret);
139+
140+
let requesterSecret;
141+
try {
142+
requesterSecret = JSON.parse(IEXEC_REQUESTER_SECRET_1);
143+
} catch {
144+
throw Error('Failed to parse requester secret');
145+
}
146+
requesterSecret = validateRequesterSecret(requesterSecret);
147+
148+
const bulkSize = parseInt(IEXEC_BULK_SLICE_SIZE, 10) || 0;
149+
150+
// Process multiple protected data
151+
if (bulkSize > 0) {
152+
console.log(`Processing ${bulkSize} protected data...`);
153+
const processPromises = new Array(bulkSize).fill(null).map((_, index) =>
154+
processProtectedData({
155+
index: index + 1,
156+
IEXEC_IN,
157+
appDeveloperSecret,
158+
requesterSecret,
159+
})
160+
);
161+
const results = await Promise.all(processPromises);
162+
const successCount = results.filter((r) => r.success === true).length;
163+
const errorCount = results.filter((r) => r.success !== true).length;
164+
result = {
165+
success: errorCount === 0,
166+
error: errorCount > 0 ? 'Partial failure' : undefined,
167+
totalCount: results.length,
168+
successCount,
169+
errorCount,
170+
results: results.map((r) => ({
171+
index: r.index,
172+
protectedData: r.protectedData,
173+
success: r.success,
174+
error: r.error,
175+
})),
176+
};
177+
} else {
178+
console.log('Processing single protected data...');
179+
const { protectedData, success, error, isEmailValidated } =
180+
await processProtectedData({
181+
index: 0,
182+
IEXEC_IN,
183+
appDeveloperSecret,
184+
requesterSecret,
185+
});
186+
187+
// Add callback data for single processing if useCallback is enabled
188+
if (requesterSecret.useCallback && isEmailValidated !== undefined) {
189+
const bool32Bytes = Buffer.alloc(32);
190+
if (isEmailValidated) {
191+
bool32Bytes[31] = 1; // set last byte to 1 for true
192+
}
193+
callbackData = `0x${bool32Bytes.toString('hex')}`;
194+
}
195+
result = { protectedData, success, error };
196+
}
197+
} catch (e) {
198+
console.error('Something went wrong:', e.message);
199+
result = { success: false, error: e.message };
200+
}
201+
202+
console.log('Writing results:', JSON.stringify(result));
203+
await fs.writeFile(
204+
`${workerEnv.IEXEC_OUT}/result.json`,
205+
JSON.stringify(result, null, 2)
206+
);
207+
208+
const computedData = {
209+
'deterministic-output-path': `${workerEnv.IEXEC_OUT}/result.json`,
210+
'callback-data': callbackData,
211+
};
212+
213+
await fs.writeFile(
214+
`${workerEnv.IEXEC_OUT}/computed.json`,
215+
JSON.stringify(computedData, null, 2)
216+
);
217+
}
218+
219+
module.exports = start;

0 commit comments

Comments
 (0)