apphx 2.0.1 → 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 +10 -8
- 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 +154 -95
- package/dist/integrations.js.map +1 -1
- package/dist/notify.js +267 -0
- package/dist/notify.js.map +1 -0
- 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/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
|