Skip to content

Commit 1fc7455

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 1fc7455

File tree

8 files changed

+699
-526
lines changed

8 files changed

+699
-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: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
const isEmailValid = 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 (!isEmailValid) {
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+
result.isEmailValid = true;
68+
} else if (mailgunResult === false) {
69+
result.isEmailValid = false;
70+
throw Error('The protected email address seems to be invalid.');
71+
} else {
72+
console.warn(
73+
'Mailgun verification failed or was unreachable. Proceeding without check.'
74+
);
75+
}
76+
} else {
77+
result.isEmailValid = true;
78+
console.log('Email already verified, skipping Mailgun check.');
79+
}
80+
81+
// Step 3: Decrypt email content
82+
const encryptedEmailContent = await downloadEncryptedContent(
83+
requesterSecret.emailContentMultiAddr
84+
);
85+
const requesterEmailContent = decryptContent(
86+
encryptedEmailContent,
87+
requesterSecret.emailContentEncryptionKey
88+
);
89+
90+
// Step 4: Send email
91+
await sendEmail({
92+
email: protectedData.email,
93+
mailJetApiKeyPublic: appDeveloperSecret.MJ_APIKEY_PUBLIC,
94+
mailJetApiKeyPrivate: appDeveloperSecret.MJ_APIKEY_PRIVATE,
95+
mailJetSender: appDeveloperSecret.MJ_SENDER,
96+
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
97+
emailContent: requesterEmailContent,
98+
emailSubject: requesterSecret.emailSubject,
99+
contentType: requesterSecret.contentType,
100+
senderName: requesterSecret.senderName,
101+
});
102+
result.success = true;
103+
} catch (e) {
104+
result.success = false;
105+
result.error = e.message;
106+
}
107+
console.log(`Protected data ${index} processed:`, result);
108+
return result;
109+
}
110+
111+
async function start() {
112+
const {
113+
IEXEC_IN,
114+
IEXEC_OUT,
115+
IEXEC_APP_DEVELOPER_SECRET,
116+
IEXEC_REQUESTER_SECRET_1,
117+
IEXEC_BULK_SLICE_SIZE,
118+
} = process.env;
119+
120+
// Check worker env
121+
const workerEnv = validateWorkerEnv({ IEXEC_OUT });
122+
123+
let result; // { success: boolean, error?: string, protectedData?: string, results?: { index: number, protectedData: string, success: boolean, error?: string }[] }
124+
let callbackData;
125+
try {
126+
// Parse the app developer secret environment variable
127+
let appDeveloperSecret;
128+
try {
129+
appDeveloperSecret = JSON.parse(IEXEC_APP_DEVELOPER_SECRET);
130+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS =
131+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS
132+
? JSON.parse(appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS)
133+
: undefined;
134+
} catch {
135+
throw Error('Failed to parse the developer secret');
136+
}
137+
appDeveloperSecret = validateAppSecret(appDeveloperSecret);
138+
139+
let requesterSecret;
140+
try {
141+
requesterSecret = JSON.parse(IEXEC_REQUESTER_SECRET_1);
142+
} catch {
143+
throw Error('Failed to parse requester secret');
144+
}
145+
requesterSecret = validateRequesterSecret(requesterSecret);
146+
147+
const bulkSize = parseInt(IEXEC_BULK_SLICE_SIZE, 10) || 0;
148+
149+
// Process multiple protected data
150+
if (bulkSize > 0) {
151+
console.log(`Processing ${bulkSize} protected data...`);
152+
const processPromises = new Array(bulkSize).fill(null).map((_, index) =>
153+
processProtectedData({
154+
index: index + 1,
155+
IEXEC_IN,
156+
appDeveloperSecret,
157+
requesterSecret,
158+
})
159+
);
160+
const results = await Promise.all(processPromises);
161+
const successCount = results.filter((r) => r.success === true).length;
162+
const errorCount = results.filter((r) => r.success !== true).length;
163+
result = {
164+
success: errorCount === 0,
165+
error: errorCount > 0 ? 'Partial failure' : undefined,
166+
totalCount: results.length,
167+
successCount,
168+
errorCount,
169+
results: results.map((r) => ({
170+
index: r.index,
171+
protectedData: r.protectedData,
172+
success: r.success,
173+
error: r.error,
174+
})),
175+
};
176+
} else {
177+
console.log('Processing single protected data...');
178+
const { protectedData, success, error, isEmailValid } =
179+
await processProtectedData({
180+
index: 0,
181+
IEXEC_IN,
182+
appDeveloperSecret,
183+
requesterSecret,
184+
});
185+
186+
// Add callback data for single processing if useCallback is enabled
187+
if (requesterSecret.useCallback && isEmailValid !== undefined) {
188+
const bool32Bytes = Buffer.alloc(32);
189+
if (isEmailValid) {
190+
bool32Bytes[31] = 1; // set last byte to 1 for true
191+
}
192+
callbackData = `0x${bool32Bytes.toString('hex')}`;
193+
}
194+
result = { protectedData, success, error };
195+
}
196+
} catch (e) {
197+
console.error('Something went wrong:', e.message);
198+
result = { success: false, error: e.message };
199+
}
200+
201+
console.log('Writing results:', JSON.stringify(result));
202+
await fs.writeFile(
203+
`${workerEnv.IEXEC_OUT}/result.json`,
204+
JSON.stringify(result, null, 2)
205+
);
206+
207+
const computedData = {
208+
'deterministic-output-path': `${workerEnv.IEXEC_OUT}/result.json`,
209+
'callback-data': callbackData,
210+
};
211+
212+
await fs.writeFile(
213+
`${workerEnv.IEXEC_OUT}/computed.json`,
214+
JSON.stringify(computedData, null, 2)
215+
);
216+
}
217+
218+
module.exports = start;

0 commit comments

Comments
 (0)