apphx 2.0.0 → 2.1.0
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/README.md +11 -9
- package/dist/cli.js +41 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/integrations.js +177 -91
- package/dist/integrations.js.map +1 -1
- package/dist/notify.js +267 -0
- package/dist/notify.js.map +1 -0
- package/dist/policy.js +3 -16
- package/dist/policy.js.map +1 -1
- package/dist/runner.js +119 -20
- package/dist/runner.js.map +1 -1
- package/dist/schemas.js +57 -2
- package/dist/schemas.js.map +1 -1
- package/dist/vendor/obfuscator/dist/config.js +119 -6
- package/dist/vendor/obfuscator/dist/config.js.map +1 -1
- package/dist/vendor/obfuscator/dist/dfa.js +528 -0
- package/dist/vendor/obfuscator/dist/dfa.js.map +1 -0
- package/dist/vendor/obfuscator/dist/flutter.js +3266 -0
- package/dist/vendor/obfuscator/dist/flutter.js.map +1 -0
- package/dist/vendor/obfuscator/dist/image-utils.js +335 -0
- package/dist/vendor/obfuscator/dist/image-utils.js.map +1 -0
- package/dist/vendor/obfuscator/dist/junk.js +1501 -0
- package/dist/vendor/obfuscator/dist/junk.js.map +1 -0
- package/dist/vendor/obfuscator/dist/lexicon-data.js +972 -0
- package/dist/vendor/obfuscator/dist/lexicon-data.js.map +1 -0
- package/dist/vendor/obfuscator/dist/obfuscate.js +51 -2
- package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -1
- package/dist/vendor/obfuscator/dist/objc-symbols.js +1819 -0
- package/dist/vendor/obfuscator/dist/objc-symbols.js.map +1 -0
- package/dist/vendor/obfuscator/dist/project-identity.js +852 -0
- package/dist/vendor/obfuscator/dist/project-identity.js.map +1 -0
- package/dist/vendor/obfuscator/dist/string-encrypt.js +330 -0
- package/dist/vendor/obfuscator/dist/string-encrypt.js.map +1 -0
- package/examples/android-java.factory.json +40 -5
- package/examples/expo.factory.json +68 -5
- package/examples/flutter.factory.json +40 -4
- package/examples/luggage.json +1 -0
- package/examples/multi-role-agents.md +129 -0
- package/examples/mygo.factory.json +54 -4
- package/examples/queue-loop-demo.md +146 -0
- package/examples/react-native.factory.json +58 -5
- package/examples/roles.example.json +31 -0
- package/examples/swift.factory.json +60 -5
- package/examples/uni-app.factory.json +58 -5
- package/package.json +1 -1
- package/skills/apphx/SKILL.md +39 -4
- package/skills/apphx/resources/requirement.json +1 -0
- package/skills/apphx-obfuscator/LICENSE +21 -0
- package/skills/apphx-obfuscator/SKILL.md +72 -0
package/dist/notify.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { toError } from "./schemas.js";
|
|
2
|
+
import { connect } from "node:net";
|
|
3
|
+
import { connect as connect$1 } from "node:tls";
|
|
4
|
+
// src/notify.ts
|
|
5
|
+
function replyFrom(lines) {
|
|
6
|
+
const status = Number(lines.at(-1)?.slice(0, 3));
|
|
7
|
+
if (!Number.isInteger(status)) throw new Error(`SMTP server sent an unreadable reply: ${JSON.stringify(lines)}`);
|
|
8
|
+
return {
|
|
9
|
+
code: status,
|
|
10
|
+
lines
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function readReply(connection, buffer) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let current = buffer;
|
|
16
|
+
const complete = [];
|
|
17
|
+
const onData = (chunk) => {
|
|
18
|
+
current += chunk.toString("utf8");
|
|
19
|
+
const lines = current.split("\r\n");
|
|
20
|
+
current = lines.pop() ?? "";
|
|
21
|
+
while (lines.length) {
|
|
22
|
+
const line = lines.shift();
|
|
23
|
+
complete.push(line);
|
|
24
|
+
if (line.length < 4 || line[3] !== "-") {
|
|
25
|
+
cleanup();
|
|
26
|
+
try {
|
|
27
|
+
resolve({
|
|
28
|
+
result: replyFrom(complete),
|
|
29
|
+
rest: current
|
|
30
|
+
});
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
reject(cause);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const onError = (error) => {
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(error);
|
|
41
|
+
};
|
|
42
|
+
const onClose = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
reject(/* @__PURE__ */ new Error("SMTP connection closed while reading a reply"));
|
|
45
|
+
};
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
connection.removeListener("data", onData);
|
|
48
|
+
connection.removeListener("error", onError);
|
|
49
|
+
connection.removeListener("close", onClose);
|
|
50
|
+
};
|
|
51
|
+
connection.on("data", onData);
|
|
52
|
+
connection.on("error", onError);
|
|
53
|
+
connection.on("close", onClose);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function expect(result, allowed, context) {
|
|
57
|
+
if (!allowed.includes(result.code)) throw new Error(`SMTP ${context} failed (${result.code}): ${result.lines.join(" ")}`);
|
|
58
|
+
}
|
|
59
|
+
async function command(connection, text, allowed, context, buffer) {
|
|
60
|
+
if (text !== null) connection.write(`${text}\r\n`);
|
|
61
|
+
const { result, rest } = await readReply(connection, buffer);
|
|
62
|
+
expect(result, allowed, context);
|
|
63
|
+
return {
|
|
64
|
+
rest,
|
|
65
|
+
result
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function connectSocket(options) {
|
|
69
|
+
if (options.secure) {
|
|
70
|
+
const socket = connect$1({
|
|
71
|
+
host: options.host,
|
|
72
|
+
port: options.port,
|
|
73
|
+
servername: options.host,
|
|
74
|
+
rejectUnauthorized: true
|
|
75
|
+
});
|
|
76
|
+
socket.setTimeout(options.timeoutMs, () => socket.destroy(/* @__PURE__ */ new Error(`SMTP TLS handshake timed out after ${options.timeoutMs}ms`)));
|
|
77
|
+
return await new Promise((resolve, reject) => {
|
|
78
|
+
socket.once("secureConnect", () => resolve(socket));
|
|
79
|
+
socket.once("error", reject);
|
|
80
|
+
socket.once("close", () => reject(/* @__PURE__ */ new Error("SMTP TLS connection closed before the handshake completed")));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const socket = connect({
|
|
84
|
+
host: options.host,
|
|
85
|
+
port: options.port
|
|
86
|
+
});
|
|
87
|
+
socket.setTimeout(options.timeoutMs, () => socket.destroy(/* @__PURE__ */ new Error(`SMTP connection timed out after ${options.timeoutMs}ms`)));
|
|
88
|
+
return await new Promise((resolve, reject) => {
|
|
89
|
+
socket.once("connect", () => resolve(socket));
|
|
90
|
+
socket.once("error", reject);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function upgradeTls(socket, options) {
|
|
94
|
+
const upgraded = connect$1({
|
|
95
|
+
socket,
|
|
96
|
+
servername: options.host,
|
|
97
|
+
rejectUnauthorized: true
|
|
98
|
+
});
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
upgraded.once("secureConnect", () => resolve());
|
|
101
|
+
upgraded.once("error", reject);
|
|
102
|
+
});
|
|
103
|
+
return upgraded;
|
|
104
|
+
}
|
|
105
|
+
/** Minimal SMTP client: EHLO, optional STARTTLS, AUTH PLAIN, MAIL/RCPT/DATA, QUIT. */
|
|
106
|
+
async function sendMail(options, envelope) {
|
|
107
|
+
let connection = await connectSocket(options);
|
|
108
|
+
try {
|
|
109
|
+
let buffer = "";
|
|
110
|
+
const greeting = await readReply(connection, buffer);
|
|
111
|
+
buffer = greeting.rest;
|
|
112
|
+
expect(greeting.result, [220], "greeting");
|
|
113
|
+
let ehlo = await command(connection, "EHLO apphx", [250], "EHLO", buffer);
|
|
114
|
+
buffer = ehlo.rest;
|
|
115
|
+
const capabilities = ehlo.result.lines.join(" ").toUpperCase();
|
|
116
|
+
if (!options.secure && capabilities.includes("STARTTLS")) {
|
|
117
|
+
buffer = (await command(connection, "STARTTLS", [220], "STARTTLS", buffer)).rest;
|
|
118
|
+
connection = await upgradeTls(connection, options);
|
|
119
|
+
ehlo = await command(connection, "EHLO apphx", [250], "EHLO", buffer);
|
|
120
|
+
buffer = ehlo.rest;
|
|
121
|
+
}
|
|
122
|
+
if (options.user) {
|
|
123
|
+
if (options.password === void 0) throw new Error("SMTP user is configured without a resolvable password; set the configured passwordEnv in the host environment");
|
|
124
|
+
const payload = Buffer.from(`\0${options.user}\0${options.password}`, "utf8").toString("base64");
|
|
125
|
+
buffer = (await command(connection, "AUTH PLAIN", [334], "AUTH challenge", buffer)).rest;
|
|
126
|
+
buffer = (await command(connection, payload, [235], "AUTH", buffer)).rest;
|
|
127
|
+
}
|
|
128
|
+
buffer = (await command(connection, `MAIL FROM:<${envelope.from}>`, [250], "MAIL FROM", buffer)).rest;
|
|
129
|
+
buffer = (await command(connection, `RCPT TO:<${envelope.to}>`, [250, 251], "RCPT TO", buffer)).rest;
|
|
130
|
+
buffer = (await command(connection, "DATA", [354], "DATA", buffer)).rest;
|
|
131
|
+
const dotStuffed = `${[
|
|
132
|
+
`From: ${envelope.from}`,
|
|
133
|
+
`To: ${envelope.to}`,
|
|
134
|
+
`Subject: ${envelope.subject}`,
|
|
135
|
+
"MIME-Version: 1.0",
|
|
136
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
137
|
+
`Message-ID: <${Date.now().toString(36)}-${Buffer.from(envelope.to).toString("hex").slice(0, 12)}@apphx.local>`
|
|
138
|
+
].join("\r\n")}\r\n\r\n${envelope.body}`.replace(/\r?\n/g, "\r\n").replace(/^\./gm, "..");
|
|
139
|
+
const accepted = await new Promise((resolve, reject) => {
|
|
140
|
+
let current = "";
|
|
141
|
+
const onData = (chunk) => {
|
|
142
|
+
current += chunk.toString("utf8");
|
|
143
|
+
const end = current.indexOf("\r\n");
|
|
144
|
+
if (end === -1) return;
|
|
145
|
+
connection.removeListener("data", onData);
|
|
146
|
+
const line = current.slice(0, end + 2);
|
|
147
|
+
const status = Number(line.slice(0, 3));
|
|
148
|
+
if (!Number.isInteger(status)) {
|
|
149
|
+
reject(/* @__PURE__ */ new Error(`SMTP DATA got an unreadable reply: ${JSON.stringify(line)}`));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
resolve({
|
|
153
|
+
code: status,
|
|
154
|
+
lines: [line.trim()]
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
connection.on("data", onData);
|
|
158
|
+
connection.write(`${dotStuffed}\r\n.\r\n`);
|
|
159
|
+
});
|
|
160
|
+
expect(accepted, [250], "DATA acceptance");
|
|
161
|
+
const queued = accepted.lines.join(" ").match(/queued as (?:<)?([^ >]+)/i);
|
|
162
|
+
await command(connection, "QUIT", [221], "QUIT", buffer).catch(() => {});
|
|
163
|
+
return { messageId: queued?.[1] ?? null };
|
|
164
|
+
} finally {
|
|
165
|
+
connection.destroy();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function template(value, job) {
|
|
169
|
+
return value.replaceAll("{product}", job.spec.id).replaceAll("{name}", job.spec.name).replaceAll("{version}", job.spec.version).replaceAll("{platform}", job.spec.platform).replaceAll("{channel}", job.spec.channel).replaceAll("{status}", job.status).replaceAll("{artifact}", job.artifact ? `${job.artifact.kind} ${job.artifact.digest}` : "none");
|
|
170
|
+
}
|
|
171
|
+
function notifyConfig(config) {
|
|
172
|
+
if (!config.notify) throw Object.assign(/* @__PURE__ */ new Error("No notify configuration; add notify.email (from, to, smtpHost, smtpPort) to the control configuration before notifying"), { code: "SETUP_REQUIRED" });
|
|
173
|
+
return config.notify.email;
|
|
174
|
+
}
|
|
175
|
+
async function notifyJob(store, config, id) {
|
|
176
|
+
const email = notifyConfig(config);
|
|
177
|
+
const job = store.transition(id, (current) => {
|
|
178
|
+
if (current.status !== "built" || !current.artifact) throw new Error("Email notification requires a built task with a recorded artifact; packaging runs after the verification and review gates, so only gate-passed tasks are notified");
|
|
179
|
+
if (current.notifiedAt) return {};
|
|
180
|
+
if (current.notifySendingAt) throw new Error("Notification already in flight for this task; retry after the current send settles");
|
|
181
|
+
return { notifySendingAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
182
|
+
});
|
|
183
|
+
if (job.notifiedAt) return {
|
|
184
|
+
id,
|
|
185
|
+
notifiedTo: job.notifiedTo ?? null,
|
|
186
|
+
messageId: null,
|
|
187
|
+
error: null
|
|
188
|
+
};
|
|
189
|
+
const to = job.spec.authorEmail ?? email.to;
|
|
190
|
+
const password = email.passwordEnv ? process.env[email.passwordEnv] : void 0;
|
|
191
|
+
const subject = template(email.subject ?? "[apphx] {name} v{version} packaged", job);
|
|
192
|
+
const body = template(email.body ?? `The local packaging pipeline finished for {name} {version} ({platform}/{channel}).\n\nArtifact: {artifact}\nStatus: {status}\n\nVerification and independent review gates passed; this email is a local notification. Store upload, submission and review remain manual human work outside this tool.`, job);
|
|
193
|
+
try {
|
|
194
|
+
const { messageId } = await sendMail({
|
|
195
|
+
host: email.smtpHost,
|
|
196
|
+
port: email.smtpPort,
|
|
197
|
+
secure: email.secure,
|
|
198
|
+
user: email.user,
|
|
199
|
+
password,
|
|
200
|
+
timeoutMs: email.timeoutMs
|
|
201
|
+
}, {
|
|
202
|
+
from: email.from,
|
|
203
|
+
to,
|
|
204
|
+
subject,
|
|
205
|
+
body
|
|
206
|
+
});
|
|
207
|
+
store.update(id, {
|
|
208
|
+
notifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
209
|
+
notifiedTo: to,
|
|
210
|
+
notifySendingAt: null,
|
|
211
|
+
notifyError: null
|
|
212
|
+
});
|
|
213
|
+
store.event(id, {
|
|
214
|
+
notified: true,
|
|
215
|
+
notifiedTo: to,
|
|
216
|
+
messageId
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
notifiedTo: to,
|
|
221
|
+
messageId,
|
|
222
|
+
error: null
|
|
223
|
+
};
|
|
224
|
+
} catch (cause) {
|
|
225
|
+
const error = toError(cause).message;
|
|
226
|
+
store.update(id, {
|
|
227
|
+
notifyError: error,
|
|
228
|
+
notifySendingAt: null
|
|
229
|
+
});
|
|
230
|
+
store.event(id, {
|
|
231
|
+
notified: false,
|
|
232
|
+
notifiedTo: to,
|
|
233
|
+
error
|
|
234
|
+
});
|
|
235
|
+
throw new Error(`Email notification failed for ${to}: ${error}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function notifyBuiltJobs(store, config) {
|
|
239
|
+
notifyConfig(config);
|
|
240
|
+
const outcomes = [];
|
|
241
|
+
for (const job of store.list()) {
|
|
242
|
+
if (job.status !== "built" || !job.artifact) continue;
|
|
243
|
+
if (job.notifiedAt) {
|
|
244
|
+
outcomes.push({
|
|
245
|
+
id: job.id,
|
|
246
|
+
notifiedTo: job.notifiedTo ?? null,
|
|
247
|
+
messageId: null,
|
|
248
|
+
error: null
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
outcomes.push(await notifyJob(store, config, job.id));
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
outcomes.push({
|
|
256
|
+
id: job.id,
|
|
257
|
+
notifiedTo: job.spec.authorEmail ?? config.notify.email.to,
|
|
258
|
+
messageId: null,
|
|
259
|
+
error: toError(cause).message
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return outcomes;
|
|
264
|
+
}
|
|
265
|
+
export { notifyBuiltJobs, notifyJob, sendMail };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","names":["tlsConnect"],"sources":["../src/notify.ts"],"sourcesContent":["import { connect, type Socket } from 'node:net';\nimport { connect as tlsConnect, type TLSSocket } from 'node:tls';\nimport { toError, type Config, type Job } from './schemas.js';\nimport type { Store } from './store.js';\n\ntype Connection = Socket | TLSSocket;\n\ninterface SmtpResult { code: number; lines: string[] }\n\nfunction replyFrom(lines: string[]): SmtpResult {\n const status = Number(lines.at(-1)?.slice(0, 3));\n if (!Number.isInteger(status)) throw new Error(`SMTP server sent an unreadable reply: ${JSON.stringify(lines)}`);\n return { code: status, lines };\n}\n\nfunction readReply(connection: Connection, buffer: string): Promise<{ result: SmtpResult; rest: string }> {\n return new Promise((resolve, reject) => {\n let current = buffer;\n const complete: string[] = [];\n const onData = (chunk: Buffer): void => {\n current += chunk.toString('utf8');\n const lines = current.split('\\r\\n');\n current = lines.pop() ?? '';\n while (lines.length) {\n const line = lines.shift()!;\n complete.push(line);\n if (line.length < 4 || line[3] !== '-') {\n cleanup();\n try { resolve({ result: replyFrom(complete), rest: current }); } catch (cause) { reject(cause); }\n return;\n }\n }\n };\n const onError = (error: Error): void => { cleanup(); reject(error); };\n const onClose = (): void => { cleanup(); reject(new Error('SMTP connection closed while reading a reply')); };\n const cleanup = (): void => {\n connection.removeListener('data', onData);\n connection.removeListener('error', onError);\n connection.removeListener('close', onClose);\n };\n connection.on('data', onData);\n connection.on('error', onError);\n connection.on('close', onClose);\n });\n}\n\nfunction expect(result: SmtpResult, allowed: number[], context: string): void {\n if (!allowed.includes(result.code)) throw new Error(`SMTP ${context} failed (${result.code}): ${result.lines.join(' ')}`);\n}\n\nasync function command(connection: Connection, text: string | null, allowed: number[], context: string, buffer: string): Promise<{ rest: string; result: SmtpResult }> {\n if (text !== null) connection.write(`${text}\\r\\n`);\n const { result, rest } = await readReply(connection, buffer);\n expect(result, allowed, context);\n return { rest, result };\n}\n\ninterface SmtpOptions {\n host: string; port: number; secure: boolean; user?: string; password?: string; timeoutMs: number;\n}\n\nasync function connectSocket(options: SmtpOptions): Promise<Connection> {\n if (options.secure) {\n const socket = tlsConnect({ host: options.host, port: options.port, servername: options.host, rejectUnauthorized: true });\n socket.setTimeout(options.timeoutMs, () => socket.destroy(new Error(`SMTP TLS handshake timed out after ${options.timeoutMs}ms`)));\n return await new Promise<Connection>((resolve, reject) => {\n socket.once('secureConnect', () => resolve(socket));\n socket.once('error', reject);\n socket.once('close', () => reject(new Error('SMTP TLS connection closed before the handshake completed')));\n });\n }\n const socket = connect({ host: options.host, port: options.port });\n socket.setTimeout(options.timeoutMs, () => socket.destroy(new Error(`SMTP connection timed out after ${options.timeoutMs}ms`)));\n return await new Promise<Connection>((resolve, reject) => {\n socket.once('connect', () => resolve(socket));\n socket.once('error', reject);\n });\n}\n\nasync function upgradeTls(socket: Connection, options: SmtpOptions): Promise<TLSSocket> {\n const upgraded = tlsConnect({ socket, servername: options.host, rejectUnauthorized: true });\n await new Promise<void>((resolve, reject) => {\n upgraded.once('secureConnect', () => resolve());\n upgraded.once('error', reject);\n });\n return upgraded;\n}\n\n/** Minimal SMTP client: EHLO, optional STARTTLS, AUTH PLAIN, MAIL/RCPT/DATA, QUIT. */\nexport async function sendMail(options: SmtpOptions, envelope: { from: string; to: string; subject: string; body: string }): Promise<{ messageId: string | null }> {\n let connection = await connectSocket(options);\n try {\n let buffer = '';\n const greeting = await readReply(connection, buffer);\n buffer = greeting.rest;\n expect(greeting.result, [220], 'greeting');\n let ehlo = await command(connection, 'EHLO apphx', [250], 'EHLO', buffer);\n buffer = ehlo.rest;\n const capabilities = ehlo.result.lines.join(' ').toUpperCase();\n if (!options.secure && capabilities.includes('STARTTLS')) {\n buffer = (await command(connection, 'STARTTLS', [220], 'STARTTLS', buffer)).rest;\n connection = await upgradeTls(connection, options);\n ehlo = await command(connection, 'EHLO apphx', [250], 'EHLO', buffer);\n buffer = ehlo.rest;\n }\n if (options.user) {\n if (options.password === undefined) throw new Error('SMTP user is configured without a resolvable password; set the configured passwordEnv in the host environment');\n const payload = Buffer.from(`\\0${options.user}\\0${options.password}`, 'utf8').toString('base64');\n buffer = (await command(connection, 'AUTH PLAIN', [334], 'AUTH challenge', buffer)).rest;\n buffer = (await command(connection, payload, [235], 'AUTH', buffer)).rest;\n }\n buffer = (await command(connection, `MAIL FROM:<${envelope.from}>`, [250], 'MAIL FROM', buffer)).rest;\n buffer = (await command(connection, `RCPT TO:<${envelope.to}>`, [250, 251], 'RCPT TO', buffer)).rest;\n buffer = (await command(connection, 'DATA', [354], 'DATA', buffer)).rest;\n const headers = [\n `From: ${envelope.from}`,\n `To: ${envelope.to}`,\n `Subject: ${envelope.subject}`,\n 'MIME-Version: 1.0',\n 'Content-Type: text/plain; charset=utf-8',\n `Message-ID: <${Date.now().toString(36)}-${Buffer.from(envelope.to).toString('hex').slice(0, 12)}@apphx.local>`,\n ].join('\\r\\n');\n const dotStuffed = `${headers}\\r\\n\\r\\n${envelope.body}`.replace(/\\r?\\n/g, '\\r\\n').replace(/^\\./gm, '..');\n const accepted = await new Promise<SmtpResult>((resolve, reject) => {\n let current = '';\n const onData = (chunk: Buffer): void => {\n current += chunk.toString('utf8');\n const end = current.indexOf('\\r\\n');\n if (end === -1) return;\n connection.removeListener('data', onData);\n const line = current.slice(0, end + 2);\n const status = Number(line.slice(0, 3));\n if (!Number.isInteger(status)) { reject(new Error(`SMTP DATA got an unreadable reply: ${JSON.stringify(line)}`)); return; }\n resolve({ code: status, lines: [line.trim()] });\n };\n connection.on('data', onData);\n connection.write(`${dotStuffed}\\r\\n.\\r\\n`);\n });\n expect(accepted, [250], 'DATA acceptance');\n const queued = accepted.lines.join(' ').match(/queued as (?:<)?([^ >]+)/i);\n await command(connection, 'QUIT', [221], 'QUIT', buffer).catch(() => {});\n return { messageId: queued?.[1] ?? null };\n } finally {\n connection.destroy();\n }\n}\n\nfunction template(value: string, job: Job): string {\n return value\n .replaceAll('{product}', job.spec.id)\n .replaceAll('{name}', job.spec.name)\n .replaceAll('{version}', job.spec.version)\n .replaceAll('{platform}', job.spec.platform)\n .replaceAll('{channel}', job.spec.channel)\n .replaceAll('{status}', job.status)\n .replaceAll('{artifact}', job.artifact ? `${job.artifact.kind} ${job.artifact.digest}` : 'none');\n}\n\nexport type NotifyOutcome = { id: string; notifiedTo: string | null; messageId: string | null; error: string | null };\n\nfunction notifyConfig(config: Config) {\n if (!config.notify) throw Object.assign(new Error('No notify configuration; add notify.email (from, to, smtpHost, smtpPort) to the control configuration before notifying'), { code: 'SETUP_REQUIRED' });\n return config.notify.email;\n}\n\nexport async function notifyJob(store: Store, config: Config, id: string): Promise<NotifyOutcome> {\n const email = notifyConfig(config);\n // Claim the send first so concurrent notifiers cannot double-send; the claim\n // is released on success (notifiedAt) or failure (notifyError).\n const job = store.transition(id, current => {\n if (current.status !== 'built' || !current.artifact) throw new Error('Email notification requires a built task with a recorded artifact; packaging runs after the verification and review gates, so only gate-passed tasks are notified');\n if (current.notifiedAt) return {};\n if (current.notifySendingAt) throw new Error('Notification already in flight for this task; retry after the current send settles');\n return { notifySendingAt: new Date().toISOString() };\n });\n if (job.notifiedAt) return { id, notifiedTo: job.notifiedTo ?? null, messageId: null, error: null };\n const to = job.spec.authorEmail ?? email.to;\n const password = email.passwordEnv ? process.env[email.passwordEnv] : undefined;\n const subject = template(email.subject ?? '[apphx] {name} v{version} packaged', job);\n const body = template(email.body ?? `The local packaging pipeline finished for {name} {version} ({platform}/{channel}).\\n\\nArtifact: {artifact}\\nStatus: {status}\\n\\nVerification and independent review gates passed; this email is a local notification. Store upload, submission and review remain manual human work outside this tool.`, job);\n try {\n const { messageId } = await sendMail({ host: email.smtpHost, port: email.smtpPort, secure: email.secure, user: email.user, password, timeoutMs: email.timeoutMs }, { from: email.from, to, subject, body });\n store.update(id, { notifiedAt: new Date().toISOString(), notifiedTo: to, notifySendingAt: null, notifyError: null });\n store.event(id, { notified: true, notifiedTo: to, messageId });\n return { id, notifiedTo: to, messageId, error: null };\n } catch (cause) {\n const error = toError(cause).message;\n store.update(id, { notifyError: error, notifySendingAt: null });\n store.event(id, { notified: false, notifiedTo: to, error });\n throw new Error(`Email notification failed for ${to}: ${error}`);\n }\n}\n\nexport async function notifyBuiltJobs(store: Store, config: Config): Promise<NotifyOutcome[]> {\n notifyConfig(config);\n const outcomes: NotifyOutcome[] = [];\n for (const job of store.list()) {\n if (job.status !== 'built' || !job.artifact) continue;\n if (job.notifiedAt) { outcomes.push({ id: job.id, notifiedTo: job.notifiedTo ?? null, messageId: null, error: null }); continue; }\n try { outcomes.push(await notifyJob(store, config, job.id)); }\n catch (cause) { outcomes.push({ id: job.id, notifiedTo: job.spec.authorEmail ?? config.notify!.email.to, messageId: null, error: toError(cause).message }); }\n }\n return outcomes;\n}\n"],"mappings":"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"}
|
package/dist/policy.js
CHANGED
|
@@ -10,26 +10,13 @@ function parseResearch(text, channel) {
|
|
|
10
10
|
}
|
|
11
11
|
return ResearchSchema.parse(value);
|
|
12
12
|
}
|
|
13
|
-
/** Honest pre-delivery differentiation and quality checklist. Store submission and review stay manual. */
|
|
14
|
-
function internalQaChecklist() {
|
|
15
|
-
return [
|
|
16
|
-
"Internal QA checklist for a normal, honest local package handoff. Store submission, account setup and platform review are manual human work outside this tool; nothing here bypasses, influences or pre-approves a store reviewer.",
|
|
17
|
-
"Real value: each package has its own genuine purpose, audience and functional behavior. Identical functionality with only renamed labels is not meaningful differentiation.",
|
|
18
|
-
"Visual identity: themes, layouts, colors and interaction flows are real product choices for this app, not templated reskins produced to look different.",
|
|
19
|
-
"Developer and identity metadata: app name, title, icon, bundle/package id, keywords and description truthfully describe this app and its actual developer. Never forge identities, accounts, locations or submission sources.",
|
|
20
|
-
"Functionality: every advertised feature works and is exercised by the frozen runtime acceptance; no hidden functionality for any audience and no fabricated screenshots or evidence.",
|
|
21
|
-
"Metadata: price, region, category and description reflect the actual product. Design rationale notes, if any, are the owner's honest answers, not scripted reply templates.",
|
|
22
|
-
"Code identity: project, folder, class, function and resource names follow this project's real conventions. Do not mutate hashes, names or resource bytes solely to defeat similarity checks; that would be deceptive.",
|
|
23
|
-
"Quality gate: source, dependency and runtime proofs must be current; independent review must cover every frozen acceptance requirement with observed evidence. Only then may the artifact be handed off to a human for store submission."
|
|
24
|
-
].join("\n");
|
|
25
|
-
}
|
|
26
13
|
function researchPrompt(job, portfolio) {
|
|
27
14
|
const siblings = `Other same-channel products ${JSON.stringify(portfolio)}; earlier versions of this same app are its source baseline, not competing products.`;
|
|
28
|
-
return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${
|
|
15
|
+
return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {"viable":boolean,"distinctness":"specific honest differentiation","risks":["..."],"plan":["concrete implementation and device acceptance step"]}. Do not promise store approval and do not add unrequested features.`;
|
|
29
16
|
}
|
|
30
17
|
function reviewInstructions(job) {
|
|
31
|
-
return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}.
|
|
18
|
+
return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {"approved":boolean,"findings":["blocking issue"],"acceptanceEvidence":[{"requirement":"exact acceptance string","evidence":"file/symbol + observed command or device scenario"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;
|
|
32
19
|
}
|
|
33
|
-
export {
|
|
20
|
+
export { parseResearch, researchPrompt, reviewInstructions };
|
|
34
21
|
|
|
35
22
|
//# sourceMappingURL=policy.js.map
|
package/dist/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["import { ResearchSchema } from './schemas.js';\nimport type { Job, Spec } from './schemas.js';\n\nexport function parseResearch(text: string, channel: Spec['channel']) {\n const content = text.trim().replace(/^```(?:json)?\\s*\\n?/, '').replace(/\\n?```$/, '');\n let value: unknown;\n try { value = JSON.parse(content); }\n catch { throw new Error('Host did not return a JSON verdict; no approval was inferred from text or an exit code'); }\n return ResearchSchema.parse(value);\n}\n\
|
|
1
|
+
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["import { ResearchSchema } from './schemas.js';\nimport type { Job, Spec } from './schemas.js';\n\nexport function parseResearch(text: string, channel: Spec['channel']) {\n const content = text.trim().replace(/^```(?:json)?\\s*\\n?/, '').replace(/\\n?```$/, '');\n let value: unknown;\n try { value = JSON.parse(content); }\n catch { throw new Error('Host did not return a JSON verdict; no approval was inferred from text or an exit code'); }\n return ResearchSchema.parse(value);\n}\n\nexport function researchPrompt(job: Job, portfolio: Array<Pick<Spec, 'id' | 'purpose' | 'audience' | 'differentiators'>>): string {\n const siblings = `Other same-channel products ${JSON.stringify(portfolio)}; earlier versions of this same app are its source baseline, not competing products.`;\n return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {\"viable\":boolean,\"distinctness\":\"specific honest differentiation\",\"risks\":[\"...\"],\"plan\":[\"concrete implementation and device acceptance step\"]}. Do not promise store approval and do not add unrequested features.`;\n}\n\nexport function reviewInstructions(job: Job): string {\n return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {\"approved\":boolean,\"findings\":[\"blocking issue\"],\"acceptanceEvidence\":[{\"requirement\":\"exact acceptance string\",\"evidence\":\"file/symbol + observed command or device scenario\"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;\n}\n"],"mappings":";;AAGA,SAAgB,cAAc,MAAc,SAA0B;CACpE,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,QAAQ,uBAAuB,EAAE,CAAC,CAAC,QAAQ,WAAW,EAAE;CACpF,IAAI;CACJ,IAAI;EAAE,QAAQ,KAAK,MAAM,OAAO;CAAG,QAC7B;EAAE,MAAM,IAAI,MAAM,wFAAwF;CAAG;CACnH,OAAO,eAAe,MAAM,KAAK;AACnC;AAEA,SAAgB,eAAe,KAAU,WAAyF;CAChI,MAAM,WAAW,+BAA+B,KAAK,UAAU,SAAS,EAAE;CAC1E,OAAO,kEAAkE,IAAI,KAAK,UAAU,GAAG,IAAI,KAAK,SAAS,0CAA0C,SAAS;AACtK;AAEA,SAAgB,mBAAmB,KAAkB;CACnD,OAAO,qKAAqK,IAAI,eAAe,2BAA2B,IAAI,yBAAyB,mBAAmB,KAAK,UAAU,IAAI,OAAO,YAAY,EAAE;AACpT"}
|
package/dist/runner.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseResearch, researchPrompt, reviewInstructions } from "./policy.js";
|
|
|
13
13
|
import { assertProject, buildInstructions } from "./project-profiles.js";
|
|
14
14
|
import { validateMacOSArtifact } from "./macos.js";
|
|
15
15
|
import { invalidateVerification } from "./proof.js";
|
|
16
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
16
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -25,6 +25,15 @@ function requireDependencyProof(job) {
|
|
|
25
25
|
function taskPrompt(job) {
|
|
26
26
|
return `You are the native host of an embedded multi-stack app Harness. Use your own context, tools and native delegation in ${projectDirectory(job)}; never launch another coding-agent CLI. Preserve the requested framework and existing architecture. Implement only this product. Never hide functionality from users or reviewers. Do not upload, submit, push, read publisher credentials or change factory state. Remote build commands belong only to the explicitly authorized Harness packaging profile, not host turns. The workspace is not a security sandbox. Complete real implementations, not placeholders.\nImmutable product requirements:\n${JSON.stringify(job.spec, null, 2)}\nPrevious verification/review failure (repair the mechanism, never weaken acceptance):\n${job.feedback ?? job.error ?? "none"}\nPersisted execution logs:\n${JSON.stringify(job.logs.slice(-3))}\nContinue existing work. Only actual deterministic checks performed by the Harness establish verification; host claims do not.\n`;
|
|
27
27
|
}
|
|
28
|
+
const DEFAULT_ROLES = {
|
|
29
|
+
research: ["product-analyst"],
|
|
30
|
+
build: ["developer"],
|
|
31
|
+
review: [
|
|
32
|
+
"architect-review",
|
|
33
|
+
"qa-engineer",
|
|
34
|
+
"acceptance-reviewer"
|
|
35
|
+
]
|
|
36
|
+
};
|
|
28
37
|
function contextDigest(job) {
|
|
29
38
|
return createHash("sha256").update(JSON.stringify({
|
|
30
39
|
spec: job.spec,
|
|
@@ -83,15 +92,18 @@ function hostAction(store, job) {
|
|
|
83
92
|
environment.FLUTTER_ROOT = job.config.toolchain.flutterSdk;
|
|
84
93
|
environment.PATH = `${join(job.config.toolchain.flutterSdk, "bin")}${process.env.PATH ? `${delimiter}${process.env.PATH}` : ""}`;
|
|
85
94
|
}
|
|
95
|
+
const role = [...job.config.roles?.[turn.stage] ?? DEFAULT_ROLES[turn.stage]];
|
|
96
|
+
const prompt = `${taskPrompt(job)}${instruction}\nTurn roles (adopt every role through your native delegation, with independent contexts where the skill requires them): ${JSON.stringify(role)}`;
|
|
86
97
|
return {
|
|
87
98
|
job,
|
|
88
99
|
action: "host",
|
|
89
100
|
turn: {
|
|
90
101
|
id: turn.id,
|
|
91
102
|
stage: turn.stage,
|
|
92
|
-
prompt
|
|
103
|
+
prompt,
|
|
93
104
|
cwd: projectDirectory(job),
|
|
94
|
-
environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0))
|
|
105
|
+
environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0)),
|
|
106
|
+
role
|
|
95
107
|
}
|
|
96
108
|
};
|
|
97
109
|
}
|
|
@@ -216,6 +228,51 @@ async function nextTurn(store, id, options = {}) {
|
|
|
216
228
|
store.release();
|
|
217
229
|
}
|
|
218
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Pick the earliest-due actionable task in the whole queue for a host loop:
|
|
233
|
+
* the first job (ordered by priority lane, then next_at, then insertion) whose
|
|
234
|
+
* `next` returns a host turn or a deterministic step. Jobs that are deferred, stopped or still
|
|
235
|
+
* owned by another execution are skipped; jobs whose persisted turn context no
|
|
236
|
+
* longer matches are surfaced in `pendingErrors` instead of silently blocking
|
|
237
|
+
* the queue. `idle` reports the earliest future due time plus a status summary
|
|
238
|
+
* so the host can audit whether the queue is truly finished or merely blocked,
|
|
239
|
+
* and knows when to wake again. Does not hold the queue worker lock across
|
|
240
|
+
* calls; each host claim stays transactional inside `nextTurn`.
|
|
241
|
+
*/
|
|
242
|
+
async function nextQueueTurn(store, options = {}) {
|
|
243
|
+
const pendingErrors = [];
|
|
244
|
+
const rank = (job) => job.spec.priority === "P0" ? 0 : job.spec.priority === "P2" ? 2 : 1;
|
|
245
|
+
const jobs = [...store.list()].sort((left, right) => rank(left) - rank(right) || left.nextAt - right.nextAt);
|
|
246
|
+
for (const job of jobs) try {
|
|
247
|
+
const action = await nextTurn(store, job.id, options);
|
|
248
|
+
if (action.action === "host" || action.action === "step") return action;
|
|
249
|
+
} catch (cause) {
|
|
250
|
+
pendingErrors.push({
|
|
251
|
+
id: job.id,
|
|
252
|
+
error: toError(cause).message
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const summary = {
|
|
256
|
+
total: jobs.length,
|
|
257
|
+
built: 0,
|
|
258
|
+
failed: 0,
|
|
259
|
+
blocked: 0,
|
|
260
|
+
waiting: 0,
|
|
261
|
+
queued: 0,
|
|
262
|
+
running: 0
|
|
263
|
+
};
|
|
264
|
+
for (const job of jobs) {
|
|
265
|
+
const status = job.status;
|
|
266
|
+
if (status === "built" || status === "failed" || status === "blocked" || status === "waiting" || status === "queued" || status === "running") summary[status] += 1;
|
|
267
|
+
}
|
|
268
|
+
const due = jobs.map((job) => job.nextAt).filter((nextAt) => nextAt > Date.now());
|
|
269
|
+
return {
|
|
270
|
+
action: "idle",
|
|
271
|
+
nextAt: due.length ? Math.min(...due) : null,
|
|
272
|
+
pendingErrors,
|
|
273
|
+
summary
|
|
274
|
+
};
|
|
275
|
+
}
|
|
219
276
|
function completeTurn(store, id, turnId, result, options = {}) {
|
|
220
277
|
return store.transition(id, (job) => {
|
|
221
278
|
if (!job.turn || job.turn.id !== turnId || job.turn.stage !== job.stage || job.status !== "running") throw new Error("Stale, replayed or wrong-phase host turn; use next to inspect the active token");
|
|
@@ -505,25 +562,67 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
505
562
|
if (job.stage === "obfuscate") {
|
|
506
563
|
const obfuscation = job.config.obfuscation;
|
|
507
564
|
if (!obfuscation) throw Object.assign(/* @__PURE__ */ new Error("Obfuscation stage requires config.obfuscation"), { code: "SETUP_REQUIRED" });
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
565
|
+
const obfuscationBase = {
|
|
566
|
+
digest: sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory),
|
|
567
|
+
path: projectDirectory(job)
|
|
568
|
+
};
|
|
569
|
+
let configPath = obfuscation.configPath;
|
|
570
|
+
let snapshot;
|
|
571
|
+
if (!configPath) {
|
|
572
|
+
const directory = join(config.stateDir, "obfuscation");
|
|
573
|
+
mkdirSync(directory, {
|
|
574
|
+
recursive: true,
|
|
575
|
+
mode: 448
|
|
576
|
+
});
|
|
577
|
+
snapshot = join(directory, `${job.id}-${Date.now()}.json`);
|
|
578
|
+
writeFileSync(snapshot, `${JSON.stringify(obfuscation.options ?? {}, null, 2)}\n`, { mode: 384 });
|
|
579
|
+
configPath = snapshot;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const expanded = expandArgv(obfuscation.command, environment);
|
|
583
|
+
const bundled = fileURLToPath(new URL("./vendor/obfuscator/dist/cli.js", import.meta.url));
|
|
584
|
+
const resolved = expanded[0] === "obfuscator" && existsSync(bundled) ? [
|
|
512
585
|
process.execPath,
|
|
513
586
|
bundled,
|
|
514
587
|
...expanded.slice(1)
|
|
515
|
-
] : expanded
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
588
|
+
] : expanded;
|
|
589
|
+
const argv = [
|
|
590
|
+
...resolved,
|
|
591
|
+
"--project",
|
|
592
|
+
projectDirectory(job),
|
|
593
|
+
"--config",
|
|
594
|
+
configPath,
|
|
595
|
+
"--framework",
|
|
596
|
+
job.spec.framework,
|
|
597
|
+
...obfuscation.seed ? ["--seed", obfuscation.seed] : []
|
|
598
|
+
];
|
|
599
|
+
const result = await trackedRun(argv, {}, { readableFiles: [configPath] });
|
|
600
|
+
if (result.code !== 0) throw new Error(`Obfuscation failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
|
|
601
|
+
let report;
|
|
602
|
+
try {
|
|
603
|
+
const parsed = JSON.parse(result.stdout);
|
|
604
|
+
if (parsed && typeof parsed === "object") {
|
|
605
|
+
const value = parsed;
|
|
606
|
+
const skipped = value.skipped && typeof value.skipped === "object" ? value.skipped : {};
|
|
607
|
+
if (Number.isSafeInteger(value.changedFiles) && value.transforms && typeof value.transforms === "object" && !Array.isArray(value.transforms)) report = {
|
|
608
|
+
changedFiles: value.changedFiles,
|
|
609
|
+
transforms: Object.fromEntries(Object.entries(value.transforms).filter(([key, count]) => typeof key === "string" && Number.isSafeInteger(count))),
|
|
610
|
+
skipped: {
|
|
611
|
+
generated: Number.isSafeInteger(skipped.generated) ? skipped.generated : 0,
|
|
612
|
+
unsupported: Number.isSafeInteger(skipped.unsupported) ? skipped.unsupported : 0
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
} catch {}
|
|
617
|
+
if (!report && resolved[0] === process.execPath && resolved[1] === bundled) throw new Error("Bundled obfuscator produced no valid report; the transformed workspace cannot be evidenced");
|
|
618
|
+
return next(store, job, "verify", {
|
|
619
|
+
obfuscationBase,
|
|
620
|
+
obfuscationDigest: sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory),
|
|
621
|
+
...report ? { obfuscationReport: report } : {}
|
|
622
|
+
});
|
|
623
|
+
} finally {
|
|
624
|
+
if (snapshot) rmSync(snapshot, { force: true });
|
|
625
|
+
}
|
|
527
626
|
}
|
|
528
627
|
if (job.stage === "package") {
|
|
529
628
|
requireReviewProof(job);
|
|
@@ -699,6 +798,6 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
699
798
|
});
|
|
700
799
|
}
|
|
701
800
|
}
|
|
702
|
-
export { abandonTurn, completeTurn, executeStep, nextTurn };
|
|
801
|
+
export { abandonTurn, completeTurn, executeStep, nextQueueTurn, nextTurn };
|
|
703
802
|
|
|
704
803
|
//# sourceMappingURL=runner.js.map
|