@crvouga/mockingbird-service-aha 0.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/dist/server.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ DEFAULT_PORT,
3
+ createServer,
4
+ serveTarget
5
+ } from "./chunk-VJVM4I3P.js";
6
+ import "./chunk-HEWJBQGW.js";
7
+ export {
8
+ DEFAULT_PORT,
9
+ createServer,
10
+ serveTarget
11
+ };
12
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
package/dist/sftp.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ type AhaSftpAccount = {
2
+ username: string;
3
+ password?: string;
4
+ publicKey?: string;
5
+ namespace?: string;
6
+ };
7
+ type AhaSftpFault = {
8
+ type: "permission" | "disconnect" | "disk-full";
9
+ } | {
10
+ type: "partial-write";
11
+ afterBytes: number;
12
+ };
13
+ type AhaSftpJournalEntry = {
14
+ at: number;
15
+ namespace: string;
16
+ account: string;
17
+ operation: string;
18
+ path?: string;
19
+ bytes?: number;
20
+ status: "ok" | "error";
21
+ };
22
+ type AhaSftpServerOptions = {
23
+ runtime: AhaSftpRuntime;
24
+ accounts?: AhaSftpAccount[];
25
+ host?: string;
26
+ port?: number;
27
+ now?: () => number;
28
+ };
29
+ /** Structural slice of the portable runtime used by the Node-only SFTP adapter. */
30
+ type AhaSftpRuntime = {
31
+ clock: {
32
+ now(): number;
33
+ };
34
+ instance(namespace?: string): {
35
+ state: {
36
+ findOrder(id: string): {
37
+ partner_order_id: string;
38
+ order_number: string;
39
+ status: string;
40
+ } | undefined;
41
+ };
42
+ transition(id: string, input: {
43
+ status: string;
44
+ }): unknown;
45
+ };
46
+ };
47
+ type AhaSftpServer = {
48
+ host: string;
49
+ port: number;
50
+ hostPublicKey: Buffer;
51
+ publishResult(orderId: string, bytes: Uint8Array, options?: {
52
+ namespace?: string;
53
+ path?: string;
54
+ }): string;
55
+ seed(path: string, bytes: Uint8Array, options?: {
56
+ namespace?: string;
57
+ account?: string;
58
+ mode?: number;
59
+ }): void;
60
+ list(options?: {
61
+ namespace?: string;
62
+ account?: string;
63
+ }): {
64
+ path: string;
65
+ size: number;
66
+ mode: number;
67
+ mtime: number;
68
+ }[];
69
+ fault(next: AhaSftpFault): void;
70
+ journal(): readonly AhaSftpJournalEntry[];
71
+ reset(namespace?: string): void;
72
+ close(): Promise<void>;
73
+ };
74
+ /** Start an actual SSH/SFTP endpoint sharing order state with an AHA HTTP runtime. */
75
+ declare function createAhaSftpServer(options: AhaSftpServerOptions): Promise<AhaSftpServer>;
76
+
77
+ export { createAhaSftpServer };
78
+ export type { AhaSftpAccount, AhaSftpFault, AhaSftpJournalEntry, AhaSftpRuntime, AhaSftpServer, AhaSftpServerOptions };
package/dist/sftp.js ADDED
@@ -0,0 +1,356 @@
1
+ // src/sftp.ts
2
+ import { createHash, createPrivateKey, createPublicKey } from "node:crypto";
3
+ import { posix } from "node:path";
4
+ import ssh2 from "ssh2";
5
+ var { Server, utils } = ssh2;
6
+ var { OPEN_MODE, STATUS_CODE } = utils.sftp;
7
+ var seedBytes = createHash("sha256").update("mockingbird-aha-sftp-host-v1").digest();
8
+ var hostKeyObject = createPrivateKey({
9
+ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seedBytes]),
10
+ format: "der",
11
+ type: "pkcs8"
12
+ });
13
+ var publicJwk = createPublicKey(hostKeyObject).export({ format: "jwk" });
14
+ if (!publicJwk.x) throw new Error("failed to derive AHA SFTP host public key");
15
+ var publicBytes = Buffer.from(publicJwk.x, "base64url");
16
+ var uint32 = (value) => {
17
+ const bytes = Buffer.alloc(4);
18
+ bytes.writeUInt32BE(value);
19
+ return bytes;
20
+ };
21
+ var sshString = (value) => {
22
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
23
+ return Buffer.concat([uint32(bytes.length), bytes]);
24
+ };
25
+ var publicBlock = Buffer.concat([sshString("ssh-ed25519"), sshString(publicBytes)]);
26
+ var privateBlock = Buffer.concat([
27
+ uint32(1296187720),
28
+ uint32(1296187720),
29
+ sshString("ssh-ed25519"),
30
+ sshString(publicBytes),
31
+ sshString(Buffer.concat([seedBytes, publicBytes])),
32
+ sshString("mockingbird-aha")
33
+ ]);
34
+ var padding = 8 - privateBlock.length % 8;
35
+ privateBlock = Buffer.concat([
36
+ privateBlock,
37
+ Buffer.from([...Array(padding)].map((_, index) => index + 1))
38
+ ]);
39
+ var encodedHostKey = Buffer.concat([
40
+ Buffer.from("openssh-key-v1\0"),
41
+ sshString("none"),
42
+ sshString("none"),
43
+ sshString(Buffer.alloc(0)),
44
+ uint32(1),
45
+ sshString(publicBlock),
46
+ sshString(privateBlock)
47
+ ]).toString("base64");
48
+ var HOST_KEY = `-----BEGIN OPENSSH PRIVATE KEY-----
49
+ ${encodedHostKey.match(/.{1,70}/g)?.join("\n")}
50
+ -----END OPENSSH PRIVATE KEY-----
51
+ `;
52
+ var normalize = (value) => posix.resolve("/", value);
53
+ var attrs = (file) => ({
54
+ mode: 32768 | file.mode,
55
+ uid: 1e3,
56
+ gid: 1e3,
57
+ size: file.bytes.length,
58
+ atime: file.mtime,
59
+ mtime: file.mtime
60
+ });
61
+ var directoryAttrs = (time) => ({
62
+ mode: 16877,
63
+ uid: 1e3,
64
+ gid: 1e3,
65
+ size: 0,
66
+ atime: time,
67
+ mtime: time
68
+ });
69
+ async function createAhaSftpServer(options) {
70
+ const now = options.now ?? options.runtime.clock.now;
71
+ const accounts = options.accounts ?? [{ username: "aha", password: "mockingbird" }];
72
+ if (accounts.length === 0) throw new Error("at least one SFTP account is required");
73
+ const files = /* @__PURE__ */ new Map();
74
+ const directories = /* @__PURE__ */ new Map();
75
+ const journal = [];
76
+ let nextFault;
77
+ const tree = (namespace, account) => {
78
+ const key = `${namespace}\0${account}`;
79
+ let records = files.get(key);
80
+ if (!records) {
81
+ records = /* @__PURE__ */ new Map();
82
+ files.set(key, records);
83
+ directories.set(key, /* @__PURE__ */ new Set(["/", "/inbox", "/outbox"]));
84
+ }
85
+ return { files: records, directories: directories.get(key) };
86
+ };
87
+ const record = (entry) => {
88
+ journal.push({ at: now(), ...entry });
89
+ if (journal.length > 1e3) journal.shift();
90
+ };
91
+ const authenticate = (context) => {
92
+ const account = accounts.find((candidate) => candidate.username === context.username);
93
+ if (context.method === "password" && account?.password === context.password)
94
+ return context.accept();
95
+ if (context.method === "publickey" && account?.publicKey) {
96
+ const parsed = utils.parseKey(account.publicKey);
97
+ const expected = Array.isArray(parsed) ? parsed[0] : parsed;
98
+ if (expected && !(expected instanceof Error) && expected.type === context.key.algo && expected.getPublicSSH().equals(context.key.data) && (!context.signature || expected.verify(context.blob, context.signature, context.hashAlgo)))
99
+ return context.accept();
100
+ }
101
+ return context.reject(["password", "publickey"]);
102
+ };
103
+ const attach = (client, account) => {
104
+ client.on("session", (accept) => {
105
+ const session = accept();
106
+ session.on("sftp", (acceptSftp) => {
107
+ const sftp = acceptSftp();
108
+ const namespace = account.namespace ?? "default";
109
+ const root = tree(namespace, account.username);
110
+ const handles = /* @__PURE__ */ new Map();
111
+ let sequence = 0;
112
+ const openHandle = (value) => {
113
+ const id = Buffer.alloc(4);
114
+ id.writeUInt32BE(++sequence);
115
+ handles.set(id.toString("hex"), value);
116
+ return id;
117
+ };
118
+ const status = (id, code, message) => sftp.status(id, code, message);
119
+ const fault = (id, operation, path) => {
120
+ const active = nextFault;
121
+ if (!active) return false;
122
+ nextFault = void 0;
123
+ record({
124
+ namespace,
125
+ account: account.username,
126
+ operation,
127
+ ...path ? { path } : {},
128
+ status: "error"
129
+ });
130
+ if (active.type === "disconnect") client.end();
131
+ else
132
+ status(
133
+ id,
134
+ active.type === "permission" ? STATUS_CODE.PERMISSION_DENIED : STATUS_CODE.FAILURE
135
+ );
136
+ return true;
137
+ };
138
+ sftp.on("REALPATH", (id, path) => {
139
+ const resolved = normalize(path);
140
+ sftp.name(id, [{ filename: resolved, longname: resolved, attrs: directoryAttrs(now()) }]);
141
+ });
142
+ sftp.on("STAT", (id, path) => {
143
+ const resolved = normalize(path);
144
+ const file = root.files.get(resolved);
145
+ if (file) sftp.attrs(id, attrs(file));
146
+ else if (root.directories.has(resolved)) sftp.attrs(id, directoryAttrs(now()));
147
+ else status(id, STATUS_CODE.NO_SUCH_FILE);
148
+ });
149
+ sftp.on("OPENDIR", (id, path) => {
150
+ const resolved = normalize(path);
151
+ if (fault(id, "opendir", resolved)) return;
152
+ if (!root.directories.has(resolved)) return status(id, STATUS_CODE.NO_SUCH_FILE);
153
+ sftp.handle(id, openHandle({ kind: "directory", path: resolved, read: false }));
154
+ });
155
+ sftp.on("READDIR", (id, raw) => {
156
+ const open = handles.get(raw.toString("hex"));
157
+ if (open?.kind !== "directory") return status(id, STATUS_CODE.FAILURE);
158
+ if (open.read) return status(id, STATUS_CODE.EOF);
159
+ open.read = true;
160
+ const entries = [];
161
+ for (const path of root.directories)
162
+ if (path !== open.path && posix.dirname(path) === open.path)
163
+ entries.push({
164
+ filename: posix.basename(path),
165
+ longname: posix.basename(path),
166
+ attrs: directoryAttrs(now())
167
+ });
168
+ for (const [path, file] of root.files)
169
+ if (posix.dirname(path) === open.path)
170
+ entries.push({
171
+ filename: posix.basename(path),
172
+ longname: posix.basename(path),
173
+ attrs: attrs(file)
174
+ });
175
+ sftp.name(id, entries);
176
+ });
177
+ sftp.on("OPEN", (id, filename, flags) => {
178
+ const path = normalize(filename);
179
+ if (fault(id, "open", path)) return;
180
+ if (!root.directories.has(posix.dirname(path)))
181
+ return status(id, STATUS_CODE.NO_SUCH_FILE);
182
+ const current = root.files.get(path);
183
+ if (!current && !(flags & OPEN_MODE.CREAT)) return status(id, STATUS_CODE.NO_SUCH_FILE);
184
+ if (current && flags & OPEN_MODE.EXCL) return status(id, STATUS_CODE.FAILURE);
185
+ if (!current || flags & OPEN_MODE.TRUNC)
186
+ root.files.set(path, {
187
+ bytes: Buffer.alloc(0),
188
+ mode: 416,
189
+ mtime: Math.floor(now() / 1e3)
190
+ });
191
+ sftp.handle(id, openHandle({ kind: "file", path, flags }));
192
+ });
193
+ sftp.on("READ", (id, raw, offset, length) => {
194
+ const open = handles.get(raw.toString("hex"));
195
+ if (open?.kind !== "file") return status(id, STATUS_CODE.FAILURE);
196
+ if (fault(id, "read", open.path)) return;
197
+ const file = root.files.get(open.path);
198
+ if (!file) return status(id, STATUS_CODE.NO_SUCH_FILE);
199
+ if (offset >= file.bytes.length) return status(id, STATUS_CODE.EOF);
200
+ const data = file.bytes.subarray(offset, Math.min(offset + length, file.bytes.length));
201
+ sftp.data(id, data);
202
+ record({
203
+ namespace,
204
+ account: account.username,
205
+ operation: "read",
206
+ path: open.path,
207
+ bytes: data.length,
208
+ status: "ok"
209
+ });
210
+ });
211
+ sftp.on("WRITE", (id, raw, offset, input) => {
212
+ const open = handles.get(raw.toString("hex"));
213
+ if (open?.kind !== "file") return status(id, STATUS_CODE.FAILURE);
214
+ if (nextFault?.type === "disk-full") return void fault(id, "write", open.path);
215
+ let data = input;
216
+ if (nextFault?.type === "partial-write") {
217
+ data = input.subarray(0, nextFault.afterBytes);
218
+ nextFault = void 0;
219
+ } else if (fault(id, "write", open.path)) return;
220
+ const file = root.files.get(open.path);
221
+ const output = Buffer.alloc(Math.max(file.bytes.length, offset + data.length));
222
+ file.bytes.copy(output);
223
+ data.copy(output, offset);
224
+ file.bytes = output;
225
+ file.mtime = Math.floor(now() / 1e3);
226
+ status(id, STATUS_CODE.OK);
227
+ record({
228
+ namespace,
229
+ account: account.username,
230
+ operation: "write",
231
+ path: open.path,
232
+ bytes: data.length,
233
+ status: "ok"
234
+ });
235
+ });
236
+ sftp.on("CLOSE", (id, raw) => {
237
+ const open = handles.get(raw.toString("hex"));
238
+ handles.delete(raw.toString("hex"));
239
+ if (open?.kind === "file") {
240
+ const file = root.files.get(open.path);
241
+ if (file?.orderId && !file.consumed && open.flags & OPEN_MODE.READ) {
242
+ file.consumed = true;
243
+ options.runtime.instance(namespace).transition(file.orderId, { status: "Lab Testing In Progress" });
244
+ }
245
+ }
246
+ status(id, STATUS_CODE.OK);
247
+ });
248
+ sftp.on("RENAME", (id, oldName, newName) => {
249
+ const oldPath = normalize(oldName);
250
+ const newPath = normalize(newName);
251
+ if (fault(id, "rename", oldPath)) return;
252
+ const file = root.files.get(oldPath);
253
+ if (!file) return status(id, STATUS_CODE.NO_SUCH_FILE);
254
+ if (root.files.has(newPath) || !root.directories.has(posix.dirname(newPath)))
255
+ return status(id, STATUS_CODE.FAILURE);
256
+ root.files.delete(oldPath);
257
+ root.files.set(newPath, file);
258
+ status(id, STATUS_CODE.OK);
259
+ });
260
+ sftp.on("REMOVE", (id, path) => {
261
+ const resolved = normalize(path);
262
+ if (fault(id, "remove", resolved)) return;
263
+ status(id, root.files.delete(resolved) ? STATUS_CODE.OK : STATUS_CODE.NO_SUCH_FILE);
264
+ });
265
+ sftp.on("MKDIR", (id, path) => {
266
+ const resolved = normalize(path);
267
+ if (fault(id, "mkdir", resolved)) return;
268
+ if (!root.directories.has(posix.dirname(resolved)))
269
+ return status(id, STATUS_CODE.NO_SUCH_FILE);
270
+ root.directories.add(resolved);
271
+ status(id, STATUS_CODE.OK);
272
+ });
273
+ });
274
+ });
275
+ };
276
+ const server = new Server(
277
+ {
278
+ hostKeys: [HOST_KEY],
279
+ ident: "SSH-2.0-Mockingbird_AHA",
280
+ algorithms: { serverHostKey: ["ssh-ed25519"] }
281
+ },
282
+ (client) => {
283
+ let authenticated;
284
+ client.on("error", () => void 0);
285
+ client.on("authentication", (context) => {
286
+ authenticated = accounts.find((account) => account.username === context.username);
287
+ authenticate(context);
288
+ });
289
+ client.on("ready", () => authenticated && attach(client, authenticated));
290
+ }
291
+ );
292
+ await new Promise((resolve, reject) => {
293
+ server.once("error", reject);
294
+ server.listen(options.port ?? 0, options.host ?? "127.0.0.1", resolve);
295
+ });
296
+ const address = server.address();
297
+ const defaultAccount = accounts[0];
298
+ const seed = (path, bytes, input = {}) => {
299
+ const root = tree(input.namespace ?? "default", input.account ?? defaultAccount.username);
300
+ const resolved = normalize(path);
301
+ if (!root.directories.has(posix.dirname(resolved)))
302
+ throw new Error(`missing SFTP directory ${posix.dirname(resolved)}`);
303
+ root.files.set(resolved, {
304
+ bytes: Buffer.from(bytes),
305
+ mode: input.mode ?? 416,
306
+ mtime: Math.floor(now() / 1e3)
307
+ });
308
+ };
309
+ const parsedHost = utils.parseKey(HOST_KEY);
310
+ if (parsedHost instanceof Error || Array.isArray(parsedHost))
311
+ throw new Error("failed to create host key");
312
+ return {
313
+ host: address.address,
314
+ port: address.port,
315
+ hostPublicKey: parsedHost.getPublicSSH(),
316
+ publishResult(orderId, bytes, input = {}) {
317
+ const namespace = input.namespace ?? "default";
318
+ const order = options.runtime.instance(namespace).state.findOrder(orderId);
319
+ if (!order) throw new Error(`no order ${orderId}`);
320
+ const path = normalize(input.path ?? `/outbox/${order.order_number}_result.pdf`);
321
+ seed(path, bytes, { namespace });
322
+ const file = tree(namespace, defaultAccount.username).files.get(path);
323
+ file.orderId = order.partner_order_id;
324
+ return path;
325
+ },
326
+ seed,
327
+ list(input = {}) {
328
+ const root = tree(input.namespace ?? "default", input.account ?? defaultAccount.username);
329
+ return [...root.files].map(([path, file]) => ({
330
+ path,
331
+ size: file.bytes.length,
332
+ mode: file.mode,
333
+ mtime: file.mtime
334
+ })).sort((left, right) => left.path.localeCompare(right.path));
335
+ },
336
+ fault(next) {
337
+ nextFault = next;
338
+ },
339
+ journal: () => journal,
340
+ reset(namespace) {
341
+ for (const key of [...files.keys()])
342
+ if (namespace === void 0 || key.startsWith(`${namespace}\0`)) files.delete(key);
343
+ for (const key of [...directories.keys()])
344
+ if (namespace === void 0 || key.startsWith(`${namespace}\0`)) directories.delete(key);
345
+ journal.length = 0;
346
+ nextFault = void 0;
347
+ },
348
+ close: () => new Promise(
349
+ (resolve, reject) => server.close((error) => error ? reject(error) : resolve())
350
+ )
351
+ };
352
+ }
353
+ export {
354
+ createAhaSftpServer
355
+ };
356
+ //# sourceMappingURL=sftp.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/sftp.ts"],
4
+ "sourcesContent": ["/// <reference types=\"node\" />\nimport { createHash, createPrivateKey, createPublicKey } from \"node:crypto\"\nimport type { AddressInfo } from \"node:net\"\nimport { posix } from \"node:path\"\nimport ssh2, { type AuthContext, type Connection, type FileEntry } from \"ssh2\"\n\nconst { Server, utils } = ssh2\nconst { OPEN_MODE, STATUS_CODE } = utils.sftp\nconst seedBytes = createHash(\"sha256\").update(\"mockingbird-aha-sftp-host-v1\").digest()\nconst hostKeyObject = createPrivateKey({\n key: Buffer.concat([Buffer.from(\"302e020100300506032b657004220420\", \"hex\"), seedBytes]),\n format: \"der\",\n type: \"pkcs8\",\n})\nconst publicJwk = createPublicKey(hostKeyObject).export({ format: \"jwk\" })\nif (!publicJwk.x) throw new Error(\"failed to derive AHA SFTP host public key\")\nconst publicBytes = Buffer.from(publicJwk.x, \"base64url\")\nconst uint32 = (value: number) => {\n const bytes = Buffer.alloc(4)\n bytes.writeUInt32BE(value)\n return bytes\n}\nconst sshString = (value: string | Buffer) => {\n const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value)\n return Buffer.concat([uint32(bytes.length), bytes])\n}\nconst publicBlock = Buffer.concat([sshString(\"ssh-ed25519\"), sshString(publicBytes)])\nlet privateBlock = Buffer.concat([\n uint32(0x4d424148),\n uint32(0x4d424148),\n sshString(\"ssh-ed25519\"),\n sshString(publicBytes),\n sshString(Buffer.concat([seedBytes, publicBytes])),\n sshString(\"mockingbird-aha\"),\n])\nconst padding = 8 - (privateBlock.length % 8)\nprivateBlock = Buffer.concat([\n privateBlock,\n Buffer.from([...Array(padding)].map((_, index) => index + 1)),\n])\nconst encodedHostKey = Buffer.concat([\n Buffer.from(\"openssh-key-v1\\0\"),\n sshString(\"none\"),\n sshString(\"none\"),\n sshString(Buffer.alloc(0)),\n uint32(1),\n sshString(publicBlock),\n sshString(privateBlock),\n]).toString(\"base64\")\nconst HOST_KEY = `-----BEGIN OPENSSH PRIVATE KEY-----\\n${encodedHostKey.match(/.{1,70}/g)?.join(\"\\n\")}\\n-----END OPENSSH PRIVATE KEY-----\\n`\n\nexport type AhaSftpAccount = {\n username: string\n password?: string\n publicKey?: string\n namespace?: string\n}\nexport type AhaSftpFault =\n | { type: \"permission\" | \"disconnect\" | \"disk-full\" }\n | { type: \"partial-write\"; afterBytes: number }\nexport type AhaSftpJournalEntry = {\n at: number\n namespace: string\n account: string\n operation: string\n path?: string\n bytes?: number\n status: \"ok\" | \"error\"\n}\nexport type AhaSftpServerOptions = {\n runtime: AhaSftpRuntime\n accounts?: AhaSftpAccount[]\n host?: string\n port?: number\n now?: () => number\n}\n/** Structural slice of the portable runtime used by the Node-only SFTP adapter. */\nexport type AhaSftpRuntime = {\n clock: { now(): number }\n instance(namespace?: string): {\n state: {\n findOrder(\n id: string,\n ): { partner_order_id: string; order_number: string; status: string } | undefined\n }\n transition(id: string, input: { status: string }): unknown\n }\n}\nexport type AhaSftpServer = {\n host: string\n port: number\n hostPublicKey: Buffer\n publishResult(\n orderId: string,\n bytes: Uint8Array,\n options?: { namespace?: string; path?: string },\n ): string\n seed(\n path: string,\n bytes: Uint8Array,\n options?: { namespace?: string; account?: string; mode?: number },\n ): void\n list(options?: {\n namespace?: string\n account?: string\n }): { path: string; size: number; mode: number; mtime: number }[]\n fault(next: AhaSftpFault): void\n journal(): readonly AhaSftpJournalEntry[]\n reset(namespace?: string): void\n close(): Promise<void>\n}\n\ntype FileRecord = {\n bytes: Buffer\n mode: number\n mtime: number\n orderId?: string\n consumed?: boolean\n}\ntype OpenHandle =\n | { kind: \"file\"; path: string; flags: number }\n | { kind: \"directory\"; path: string; read: boolean }\n\nconst normalize = (value: string) => posix.resolve(\"/\", value)\nconst attrs = (file: FileRecord) => ({\n mode: 0o100000 | file.mode,\n uid: 1000,\n gid: 1000,\n size: file.bytes.length,\n atime: file.mtime,\n mtime: file.mtime,\n})\nconst directoryAttrs = (time: number) => ({\n mode: 0o040755,\n uid: 1000,\n gid: 1000,\n size: 0,\n atime: time,\n mtime: time,\n})\n\n/** Start an actual SSH/SFTP endpoint sharing order state with an AHA HTTP runtime. */\nexport async function createAhaSftpServer(options: AhaSftpServerOptions): Promise<AhaSftpServer> {\n const now = options.now ?? options.runtime.clock.now\n const accounts = options.accounts ?? [{ username: \"aha\", password: \"mockingbird\" }]\n if (accounts.length === 0) throw new Error(\"at least one SFTP account is required\")\n const files = new Map<string, Map<string, FileRecord>>()\n const directories = new Map<string, Set<string>>()\n const journal: AhaSftpJournalEntry[] = []\n let nextFault: AhaSftpFault | undefined\n const tree = (namespace: string, account: string) => {\n const key = `${namespace}\\0${account}`\n let records = files.get(key)\n if (!records) {\n records = new Map()\n files.set(key, records)\n directories.set(key, new Set([\"/\", \"/inbox\", \"/outbox\"]))\n }\n return { files: records, directories: directories.get(key) as Set<string> }\n }\n const record = (entry: Omit<AhaSftpJournalEntry, \"at\">) => {\n journal.push({ at: now(), ...entry })\n if (journal.length > 1000) journal.shift()\n }\n const authenticate = (context: AuthContext) => {\n const account = accounts.find((candidate) => candidate.username === context.username)\n if (context.method === \"password\" && account?.password === context.password)\n return context.accept()\n if (context.method === \"publickey\" && account?.publicKey) {\n const parsed = utils.parseKey(account.publicKey)\n const expected = Array.isArray(parsed) ? parsed[0] : parsed\n if (\n expected &&\n !(expected instanceof Error) &&\n expected.type === context.key.algo &&\n expected.getPublicSSH().equals(context.key.data) &&\n (!context.signature || expected.verify(context.blob, context.signature, context.hashAlgo))\n )\n return context.accept()\n }\n return context.reject([\"password\", \"publickey\"])\n }\n const attach = (client: Connection, account: AhaSftpAccount) => {\n client.on(\"session\", (accept) => {\n const session = accept()\n session.on(\"sftp\", (acceptSftp) => {\n const sftp = acceptSftp()\n const namespace = account.namespace ?? \"default\"\n const root = tree(namespace, account.username)\n const handles = new Map<string, OpenHandle>()\n let sequence = 0\n const openHandle = (value: OpenHandle) => {\n const id = Buffer.alloc(4)\n id.writeUInt32BE(++sequence)\n handles.set(id.toString(\"hex\"), value)\n return id\n }\n const status = (id: number, code: number, message?: string) =>\n sftp.status(id, code, message)\n const fault = (id: number, operation: string, path?: string) => {\n const active = nextFault\n if (!active) return false\n nextFault = undefined\n record({\n namespace,\n account: account.username,\n operation,\n ...(path ? { path } : {}),\n status: \"error\",\n })\n if (active.type === \"disconnect\") client.end()\n else\n status(\n id,\n active.type === \"permission\" ? STATUS_CODE.PERMISSION_DENIED : STATUS_CODE.FAILURE,\n )\n return true\n }\n sftp.on(\"REALPATH\", (id, path) => {\n const resolved = normalize(path)\n sftp.name(id, [{ filename: resolved, longname: resolved, attrs: directoryAttrs(now()) }])\n })\n sftp.on(\"STAT\", (id, path) => {\n const resolved = normalize(path)\n const file = root.files.get(resolved)\n if (file) sftp.attrs(id, attrs(file))\n else if (root.directories.has(resolved)) sftp.attrs(id, directoryAttrs(now()))\n else status(id, STATUS_CODE.NO_SUCH_FILE)\n })\n sftp.on(\"OPENDIR\", (id, path) => {\n const resolved = normalize(path)\n if (fault(id, \"opendir\", resolved)) return\n if (!root.directories.has(resolved)) return status(id, STATUS_CODE.NO_SUCH_FILE)\n sftp.handle(id, openHandle({ kind: \"directory\", path: resolved, read: false }))\n })\n sftp.on(\"READDIR\", (id, raw) => {\n const open = handles.get(raw.toString(\"hex\"))\n if (open?.kind !== \"directory\") return status(id, STATUS_CODE.FAILURE)\n if (open.read) return status(id, STATUS_CODE.EOF)\n open.read = true\n const entries: FileEntry[] = []\n for (const path of root.directories)\n if (path !== open.path && posix.dirname(path) === open.path)\n entries.push({\n filename: posix.basename(path),\n longname: posix.basename(path),\n attrs: directoryAttrs(now()),\n })\n for (const [path, file] of root.files)\n if (posix.dirname(path) === open.path)\n entries.push({\n filename: posix.basename(path),\n longname: posix.basename(path),\n attrs: attrs(file),\n })\n sftp.name(id, entries)\n })\n sftp.on(\"OPEN\", (id, filename, flags) => {\n const path = normalize(filename)\n if (fault(id, \"open\", path)) return\n if (!root.directories.has(posix.dirname(path)))\n return status(id, STATUS_CODE.NO_SUCH_FILE)\n const current = root.files.get(path)\n if (!current && !(flags & OPEN_MODE.CREAT)) return status(id, STATUS_CODE.NO_SUCH_FILE)\n if (current && flags & OPEN_MODE.EXCL) return status(id, STATUS_CODE.FAILURE)\n if (!current || flags & OPEN_MODE.TRUNC)\n root.files.set(path, {\n bytes: Buffer.alloc(0),\n mode: 0o640,\n mtime: Math.floor(now() / 1000),\n })\n sftp.handle(id, openHandle({ kind: \"file\", path, flags }))\n })\n sftp.on(\"READ\", (id, raw, offset, length) => {\n const open = handles.get(raw.toString(\"hex\"))\n if (open?.kind !== \"file\") return status(id, STATUS_CODE.FAILURE)\n if (fault(id, \"read\", open.path)) return\n const file = root.files.get(open.path)\n if (!file) return status(id, STATUS_CODE.NO_SUCH_FILE)\n if (offset >= file.bytes.length) return status(id, STATUS_CODE.EOF)\n const data = file.bytes.subarray(offset, Math.min(offset + length, file.bytes.length))\n sftp.data(id, data)\n record({\n namespace,\n account: account.username,\n operation: \"read\",\n path: open.path,\n bytes: data.length,\n status: \"ok\",\n })\n })\n sftp.on(\"WRITE\", (id, raw, offset, input) => {\n const open = handles.get(raw.toString(\"hex\"))\n if (open?.kind !== \"file\") return status(id, STATUS_CODE.FAILURE)\n if (nextFault?.type === \"disk-full\") return void fault(id, \"write\", open.path)\n let data = input\n if (nextFault?.type === \"partial-write\") {\n data = input.subarray(0, nextFault.afterBytes)\n nextFault = undefined\n } else if (fault(id, \"write\", open.path)) return\n const file = root.files.get(open.path) as FileRecord\n const output = Buffer.alloc(Math.max(file.bytes.length, offset + data.length))\n file.bytes.copy(output)\n data.copy(output, offset)\n file.bytes = output\n file.mtime = Math.floor(now() / 1000)\n status(id, STATUS_CODE.OK)\n record({\n namespace,\n account: account.username,\n operation: \"write\",\n path: open.path,\n bytes: data.length,\n status: \"ok\",\n })\n })\n sftp.on(\"CLOSE\", (id, raw) => {\n const open = handles.get(raw.toString(\"hex\"))\n handles.delete(raw.toString(\"hex\"))\n if (open?.kind === \"file\") {\n const file = root.files.get(open.path)\n if (file?.orderId && !file.consumed && open.flags & OPEN_MODE.READ) {\n file.consumed = true\n options.runtime\n .instance(namespace)\n .transition(file.orderId, { status: \"Lab Testing In Progress\" })\n }\n }\n status(id, STATUS_CODE.OK)\n })\n sftp.on(\"RENAME\", (id, oldName, newName) => {\n const oldPath = normalize(oldName)\n const newPath = normalize(newName)\n if (fault(id, \"rename\", oldPath)) return\n const file = root.files.get(oldPath)\n if (!file) return status(id, STATUS_CODE.NO_SUCH_FILE)\n if (root.files.has(newPath) || !root.directories.has(posix.dirname(newPath)))\n return status(id, STATUS_CODE.FAILURE)\n root.files.delete(oldPath)\n root.files.set(newPath, file)\n status(id, STATUS_CODE.OK)\n })\n sftp.on(\"REMOVE\", (id, path) => {\n const resolved = normalize(path)\n if (fault(id, \"remove\", resolved)) return\n status(id, root.files.delete(resolved) ? STATUS_CODE.OK : STATUS_CODE.NO_SUCH_FILE)\n })\n sftp.on(\"MKDIR\", (id, path) => {\n const resolved = normalize(path)\n if (fault(id, \"mkdir\", resolved)) return\n if (!root.directories.has(posix.dirname(resolved)))\n return status(id, STATUS_CODE.NO_SUCH_FILE)\n root.directories.add(resolved)\n status(id, STATUS_CODE.OK)\n })\n })\n })\n }\n const server = new Server(\n {\n hostKeys: [HOST_KEY],\n ident: \"SSH-2.0-Mockingbird_AHA\",\n algorithms: { serverHostKey: [\"ssh-ed25519\"] },\n },\n (client) => {\n let authenticated: AhaSftpAccount | undefined\n // Authentication and host-key rejection are expected negative-test paths.\n client.on(\"error\", () => undefined)\n client.on(\"authentication\", (context) => {\n authenticated = accounts.find((account) => account.username === context.username)\n authenticate(context)\n })\n client.on(\"ready\", () => authenticated && attach(client, authenticated))\n },\n )\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host ?? \"127.0.0.1\", resolve)\n })\n const address = server.address() as AddressInfo\n const defaultAccount = accounts[0] as AhaSftpAccount\n const seed = (\n path: string,\n bytes: Uint8Array,\n input: { namespace?: string; account?: string; mode?: number } = {},\n ) => {\n const root = tree(input.namespace ?? \"default\", input.account ?? defaultAccount.username)\n const resolved = normalize(path)\n if (!root.directories.has(posix.dirname(resolved)))\n throw new Error(`missing SFTP directory ${posix.dirname(resolved)}`)\n root.files.set(resolved, {\n bytes: Buffer.from(bytes),\n mode: input.mode ?? 0o640,\n mtime: Math.floor(now() / 1000),\n })\n }\n const parsedHost = utils.parseKey(HOST_KEY)\n if (parsedHost instanceof Error || Array.isArray(parsedHost))\n throw new Error(\"failed to create host key\")\n return {\n host: address.address,\n port: address.port,\n hostPublicKey: parsedHost.getPublicSSH(),\n publishResult(orderId, bytes, input = {}) {\n const namespace = input.namespace ?? \"default\"\n const order = options.runtime.instance(namespace).state.findOrder(orderId)\n if (!order) throw new Error(`no order ${orderId}`)\n const path = normalize(input.path ?? `/outbox/${order.order_number}_result.pdf`)\n seed(path, bytes, { namespace })\n const file = tree(namespace, defaultAccount.username).files.get(path) as FileRecord\n file.orderId = order.partner_order_id\n return path\n },\n seed,\n list(input = {}) {\n const root = tree(input.namespace ?? \"default\", input.account ?? defaultAccount.username)\n return [...root.files]\n .map(([path, file]) => ({\n path,\n size: file.bytes.length,\n mode: file.mode,\n mtime: file.mtime,\n }))\n .sort((left, right) => left.path.localeCompare(right.path))\n },\n fault(next) {\n nextFault = next\n },\n journal: () => journal,\n reset(namespace) {\n for (const key of [...files.keys()])\n if (namespace === undefined || key.startsWith(`${namespace}\\0`)) files.delete(key)\n for (const key of [...directories.keys()])\n if (namespace === undefined || key.startsWith(`${namespace}\\0`)) directories.delete(key)\n journal.length = 0\n nextFault = undefined\n },\n close: () =>\n new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n ),\n }\n}\n"],
5
+ "mappings": ";AACA,SAAS,YAAY,kBAAkB,uBAAuB;AAE9D,SAAS,aAAa;AACtB,OAAO,UAAiE;AAExE,IAAM,EAAE,QAAQ,MAAM,IAAI;AAC1B,IAAM,EAAE,WAAW,YAAY,IAAI,MAAM;AACzC,IAAM,YAAY,WAAW,QAAQ,EAAE,OAAO,8BAA8B,EAAE,OAAO;AACrF,IAAM,gBAAgB,iBAAiB;AAAA,EACrC,KAAK,OAAO,OAAO,CAAC,OAAO,KAAK,oCAAoC,KAAK,GAAG,SAAS,CAAC;AAAA,EACtF,QAAQ;AAAA,EACR,MAAM;AACR,CAAC;AACD,IAAM,YAAY,gBAAgB,aAAa,EAAE,OAAO,EAAE,QAAQ,MAAM,CAAC;AACzE,IAAI,CAAC,UAAU,EAAG,OAAM,IAAI,MAAM,2CAA2C;AAC7E,IAAM,cAAc,OAAO,KAAK,UAAU,GAAG,WAAW;AACxD,IAAM,SAAS,CAAC,UAAkB;AAChC,QAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,QAAM,cAAc,KAAK;AACzB,SAAO;AACT;AACA,IAAM,YAAY,CAAC,UAA2B;AAC5C,QAAM,QAAQ,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK;AAChE,SAAO,OAAO,OAAO,CAAC,OAAO,MAAM,MAAM,GAAG,KAAK,CAAC;AACpD;AACA,IAAM,cAAc,OAAO,OAAO,CAAC,UAAU,aAAa,GAAG,UAAU,WAAW,CAAC,CAAC;AACpF,IAAI,eAAe,OAAO,OAAO;AAAA,EAC/B,OAAO,UAAU;AAAA,EACjB,OAAO,UAAU;AAAA,EACjB,UAAU,aAAa;AAAA,EACvB,UAAU,WAAW;AAAA,EACrB,UAAU,OAAO,OAAO,CAAC,WAAW,WAAW,CAAC,CAAC;AAAA,EACjD,UAAU,iBAAiB;AAC7B,CAAC;AACD,IAAM,UAAU,IAAK,aAAa,SAAS;AAC3C,eAAe,OAAO,OAAO;AAAA,EAC3B;AAAA,EACA,OAAO,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,CAAC,GAAG,UAAU,QAAQ,CAAC,CAAC;AAC9D,CAAC;AACD,IAAM,iBAAiB,OAAO,OAAO;AAAA,EACnC,OAAO,KAAK,kBAAkB;AAAA,EAC9B,UAAU,MAAM;AAAA,EAChB,UAAU,MAAM;AAAA,EAChB,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EACzB,OAAO,CAAC;AAAA,EACR,UAAU,WAAW;AAAA,EACrB,UAAU,YAAY;AACxB,CAAC,EAAE,SAAS,QAAQ;AACpB,IAAM,WAAW;AAAA,EAAwC,eAAe,MAAM,UAAU,GAAG,KAAK,IAAI,CAAC;AAAA;AAAA;AA0ErG,IAAM,YAAY,CAAC,UAAkB,MAAM,QAAQ,KAAK,KAAK;AAC7D,IAAM,QAAQ,CAAC,UAAsB;AAAA,EACnC,MAAM,QAAW,KAAK;AAAA,EACtB,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM,KAAK,MAAM;AAAA,EACjB,OAAO,KAAK;AAAA,EACZ,OAAO,KAAK;AACd;AACA,IAAM,iBAAiB,CAAC,UAAkB;AAAA,EACxC,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AACT;AAGA,eAAsB,oBAAoB,SAAuD;AAC/F,QAAM,MAAM,QAAQ,OAAO,QAAQ,QAAQ,MAAM;AACjD,QAAM,WAAW,QAAQ,YAAY,CAAC,EAAE,UAAU,OAAO,UAAU,cAAc,CAAC;AAClF,MAAI,SAAS,WAAW,EAAG,OAAM,IAAI,MAAM,uCAAuC;AAClF,QAAM,QAAQ,oBAAI,IAAqC;AACvD,QAAM,cAAc,oBAAI,IAAyB;AACjD,QAAM,UAAiC,CAAC;AACxC,MAAI;AACJ,QAAM,OAAO,CAAC,WAAmB,YAAoB;AACnD,UAAM,MAAM,GAAG,SAAS,KAAK,OAAO;AACpC,QAAI,UAAU,MAAM,IAAI,GAAG;AAC3B,QAAI,CAAC,SAAS;AACZ,gBAAU,oBAAI,IAAI;AAClB,YAAM,IAAI,KAAK,OAAO;AACtB,kBAAY,IAAI,KAAK,oBAAI,IAAI,CAAC,KAAK,UAAU,SAAS,CAAC,CAAC;AAAA,IAC1D;AACA,WAAO,EAAE,OAAO,SAAS,aAAa,YAAY,IAAI,GAAG,EAAiB;AAAA,EAC5E;AACA,QAAM,SAAS,CAAC,UAA2C;AACzD,YAAQ,KAAK,EAAE,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC;AACpC,QAAI,QAAQ,SAAS,IAAM,SAAQ,MAAM;AAAA,EAC3C;AACA,QAAM,eAAe,CAAC,YAAyB;AAC7C,UAAM,UAAU,SAAS,KAAK,CAAC,cAAc,UAAU,aAAa,QAAQ,QAAQ;AACpF,QAAI,QAAQ,WAAW,cAAc,SAAS,aAAa,QAAQ;AACjE,aAAO,QAAQ,OAAO;AACxB,QAAI,QAAQ,WAAW,eAAe,SAAS,WAAW;AACxD,YAAM,SAAS,MAAM,SAAS,QAAQ,SAAS;AAC/C,YAAM,WAAW,MAAM,QAAQ,MAAM,IAAI,OAAO,CAAC,IAAI;AACrD,UACE,YACA,EAAE,oBAAoB,UACtB,SAAS,SAAS,QAAQ,IAAI,QAC9B,SAAS,aAAa,EAAE,OAAO,QAAQ,IAAI,IAAI,MAC9C,CAAC,QAAQ,aAAa,SAAS,OAAO,QAAQ,MAAM,QAAQ,WAAW,QAAQ,QAAQ;AAExF,eAAO,QAAQ,OAAO;AAAA,IAC1B;AACA,WAAO,QAAQ,OAAO,CAAC,YAAY,WAAW,CAAC;AAAA,EACjD;AACA,QAAM,SAAS,CAAC,QAAoB,YAA4B;AAC9D,WAAO,GAAG,WAAW,CAAC,WAAW;AAC/B,YAAM,UAAU,OAAO;AACvB,cAAQ,GAAG,QAAQ,CAAC,eAAe;AACjC,cAAM,OAAO,WAAW;AACxB,cAAM,YAAY,QAAQ,aAAa;AACvC,cAAM,OAAO,KAAK,WAAW,QAAQ,QAAQ;AAC7C,cAAM,UAAU,oBAAI,IAAwB;AAC5C,YAAI,WAAW;AACf,cAAM,aAAa,CAAC,UAAsB;AACxC,gBAAM,KAAK,OAAO,MAAM,CAAC;AACzB,aAAG,cAAc,EAAE,QAAQ;AAC3B,kBAAQ,IAAI,GAAG,SAAS,KAAK,GAAG,KAAK;AACrC,iBAAO;AAAA,QACT;AACA,cAAM,SAAS,CAAC,IAAY,MAAc,YACxC,KAAK,OAAO,IAAI,MAAM,OAAO;AAC/B,cAAM,QAAQ,CAAC,IAAY,WAAmB,SAAkB;AAC9D,gBAAM,SAAS;AACf,cAAI,CAAC,OAAQ,QAAO;AACpB,sBAAY;AACZ,iBAAO;AAAA,YACL;AAAA,YACA,SAAS,QAAQ;AAAA,YACjB;AAAA,YACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,YACvB,QAAQ;AAAA,UACV,CAAC;AACD,cAAI,OAAO,SAAS,aAAc,QAAO,IAAI;AAAA;AAE3C;AAAA,cACE;AAAA,cACA,OAAO,SAAS,eAAe,YAAY,oBAAoB,YAAY;AAAA,YAC7E;AACF,iBAAO;AAAA,QACT;AACA,aAAK,GAAG,YAAY,CAAC,IAAI,SAAS;AAChC,gBAAM,WAAW,UAAU,IAAI;AAC/B,eAAK,KAAK,IAAI,CAAC,EAAE,UAAU,UAAU,UAAU,UAAU,OAAO,eAAe,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,QAC1F,CAAC;AACD,aAAK,GAAG,QAAQ,CAAC,IAAI,SAAS;AAC5B,gBAAM,WAAW,UAAU,IAAI;AAC/B,gBAAM,OAAO,KAAK,MAAM,IAAI,QAAQ;AACpC,cAAI,KAAM,MAAK,MAAM,IAAI,MAAM,IAAI,CAAC;AAAA,mBAC3B,KAAK,YAAY,IAAI,QAAQ,EAAG,MAAK,MAAM,IAAI,eAAe,IAAI,CAAC,CAAC;AAAA,cACxE,QAAO,IAAI,YAAY,YAAY;AAAA,QAC1C,CAAC;AACD,aAAK,GAAG,WAAW,CAAC,IAAI,SAAS;AAC/B,gBAAM,WAAW,UAAU,IAAI;AAC/B,cAAI,MAAM,IAAI,WAAW,QAAQ,EAAG;AACpC,cAAI,CAAC,KAAK,YAAY,IAAI,QAAQ,EAAG,QAAO,OAAO,IAAI,YAAY,YAAY;AAC/E,eAAK,OAAO,IAAI,WAAW,EAAE,MAAM,aAAa,MAAM,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,QAChF,CAAC;AACD,aAAK,GAAG,WAAW,CAAC,IAAI,QAAQ;AAC9B,gBAAM,OAAO,QAAQ,IAAI,IAAI,SAAS,KAAK,CAAC;AAC5C,cAAI,MAAM,SAAS,YAAa,QAAO,OAAO,IAAI,YAAY,OAAO;AACrE,cAAI,KAAK,KAAM,QAAO,OAAO,IAAI,YAAY,GAAG;AAChD,eAAK,OAAO;AACZ,gBAAM,UAAuB,CAAC;AAC9B,qBAAW,QAAQ,KAAK;AACtB,gBAAI,SAAS,KAAK,QAAQ,MAAM,QAAQ,IAAI,MAAM,KAAK;AACrD,sBAAQ,KAAK;AAAA,gBACX,UAAU,MAAM,SAAS,IAAI;AAAA,gBAC7B,UAAU,MAAM,SAAS,IAAI;AAAA,gBAC7B,OAAO,eAAe,IAAI,CAAC;AAAA,cAC7B,CAAC;AACL,qBAAW,CAAC,MAAM,IAAI,KAAK,KAAK;AAC9B,gBAAI,MAAM,QAAQ,IAAI,MAAM,KAAK;AAC/B,sBAAQ,KAAK;AAAA,gBACX,UAAU,MAAM,SAAS,IAAI;AAAA,gBAC7B,UAAU,MAAM,SAAS,IAAI;AAAA,gBAC7B,OAAO,MAAM,IAAI;AAAA,cACnB,CAAC;AACL,eAAK,KAAK,IAAI,OAAO;AAAA,QACvB,CAAC;AACD,aAAK,GAAG,QAAQ,CAAC,IAAI,UAAU,UAAU;AACvC,gBAAM,OAAO,UAAU,QAAQ;AAC/B,cAAI,MAAM,IAAI,QAAQ,IAAI,EAAG;AAC7B,cAAI,CAAC,KAAK,YAAY,IAAI,MAAM,QAAQ,IAAI,CAAC;AAC3C,mBAAO,OAAO,IAAI,YAAY,YAAY;AAC5C,gBAAM,UAAU,KAAK,MAAM,IAAI,IAAI;AACnC,cAAI,CAAC,WAAW,EAAE,QAAQ,UAAU,OAAQ,QAAO,OAAO,IAAI,YAAY,YAAY;AACtF,cAAI,WAAW,QAAQ,UAAU,KAAM,QAAO,OAAO,IAAI,YAAY,OAAO;AAC5E,cAAI,CAAC,WAAW,QAAQ,UAAU;AAChC,iBAAK,MAAM,IAAI,MAAM;AAAA,cACnB,OAAO,OAAO,MAAM,CAAC;AAAA,cACrB,MAAM;AAAA,cACN,OAAO,KAAK,MAAM,IAAI,IAAI,GAAI;AAAA,YAChC,CAAC;AACH,eAAK,OAAO,IAAI,WAAW,EAAE,MAAM,QAAQ,MAAM,MAAM,CAAC,CAAC;AAAA,QAC3D,CAAC;AACD,aAAK,GAAG,QAAQ,CAAC,IAAI,KAAK,QAAQ,WAAW;AAC3C,gBAAM,OAAO,QAAQ,IAAI,IAAI,SAAS,KAAK,CAAC;AAC5C,cAAI,MAAM,SAAS,OAAQ,QAAO,OAAO,IAAI,YAAY,OAAO;AAChE,cAAI,MAAM,IAAI,QAAQ,KAAK,IAAI,EAAG;AAClC,gBAAM,OAAO,KAAK,MAAM,IAAI,KAAK,IAAI;AACrC,cAAI,CAAC,KAAM,QAAO,OAAO,IAAI,YAAY,YAAY;AACrD,cAAI,UAAU,KAAK,MAAM,OAAQ,QAAO,OAAO,IAAI,YAAY,GAAG;AAClE,gBAAM,OAAO,KAAK,MAAM,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,KAAK,MAAM,MAAM,CAAC;AACrF,eAAK,KAAK,IAAI,IAAI;AAClB,iBAAO;AAAA,YACL;AAAA,YACA,SAAS,QAAQ;AAAA,YACjB,WAAW;AAAA,YACX,MAAM,KAAK;AAAA,YACX,OAAO,KAAK;AAAA,YACZ,QAAQ;AAAA,UACV,CAAC;AAAA,QACH,CAAC;AACD,aAAK,GAAG,SAAS,CAAC,IAAI,KAAK,QAAQ,UAAU;AAC3C,gBAAM,OAAO,QAAQ,IAAI,IAAI,SAAS,KAAK,CAAC;AAC5C,cAAI,MAAM,SAAS,OAAQ,QAAO,OAAO,IAAI,YAAY,OAAO;AAChE,cAAI,WAAW,SAAS,YAAa,QAAO,KAAK,MAAM,IAAI,SAAS,KAAK,IAAI;AAC7E,cAAI,OAAO;AACX,cAAI,WAAW,SAAS,iBAAiB;AACvC,mBAAO,MAAM,SAAS,GAAG,UAAU,UAAU;AAC7C,wBAAY;AAAA,UACd,WAAW,MAAM,IAAI,SAAS,KAAK,IAAI,EAAG;AAC1C,gBAAM,OAAO,KAAK,MAAM,IAAI,KAAK,IAAI;AACrC,gBAAM,SAAS,OAAO,MAAM,KAAK,IAAI,KAAK,MAAM,QAAQ,SAAS,KAAK,MAAM,CAAC;AAC7E,eAAK,MAAM,KAAK,MAAM;AACtB,eAAK,KAAK,QAAQ,MAAM;AACxB,eAAK,QAAQ;AACb,eAAK,QAAQ,KAAK,MAAM,IAAI,IAAI,GAAI;AACpC,iBAAO,IAAI,YAAY,EAAE;AACzB,iBAAO;AAAA,YACL;AAAA,YACA,SAAS,QAAQ;AAAA,YACjB,WAAW;AAAA,YACX,MAAM,KAAK;AAAA,YACX,OAAO,KAAK;AAAA,YACZ,QAAQ;AAAA,UACV,CAAC;AAAA,QACH,CAAC;AACD,aAAK,GAAG,SAAS,CAAC,IAAI,QAAQ;AAC5B,gBAAM,OAAO,QAAQ,IAAI,IAAI,SAAS,KAAK,CAAC;AAC5C,kBAAQ,OAAO,IAAI,SAAS,KAAK,CAAC;AAClC,cAAI,MAAM,SAAS,QAAQ;AACzB,kBAAM,OAAO,KAAK,MAAM,IAAI,KAAK,IAAI;AACrC,gBAAI,MAAM,WAAW,CAAC,KAAK,YAAY,KAAK,QAAQ,UAAU,MAAM;AAClE,mBAAK,WAAW;AAChB,sBAAQ,QACL,SAAS,SAAS,EAClB,WAAW,KAAK,SAAS,EAAE,QAAQ,0BAA0B,CAAC;AAAA,YACnE;AAAA,UACF;AACA,iBAAO,IAAI,YAAY,EAAE;AAAA,QAC3B,CAAC;AACD,aAAK,GAAG,UAAU,CAAC,IAAI,SAAS,YAAY;AAC1C,gBAAM,UAAU,UAAU,OAAO;AACjC,gBAAM,UAAU,UAAU,OAAO;AACjC,cAAI,MAAM,IAAI,UAAU,OAAO,EAAG;AAClC,gBAAM,OAAO,KAAK,MAAM,IAAI,OAAO;AACnC,cAAI,CAAC,KAAM,QAAO,OAAO,IAAI,YAAY,YAAY;AACrD,cAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,KAAK,YAAY,IAAI,MAAM,QAAQ,OAAO,CAAC;AACzE,mBAAO,OAAO,IAAI,YAAY,OAAO;AACvC,eAAK,MAAM,OAAO,OAAO;AACzB,eAAK,MAAM,IAAI,SAAS,IAAI;AAC5B,iBAAO,IAAI,YAAY,EAAE;AAAA,QAC3B,CAAC;AACD,aAAK,GAAG,UAAU,CAAC,IAAI,SAAS;AAC9B,gBAAM,WAAW,UAAU,IAAI;AAC/B,cAAI,MAAM,IAAI,UAAU,QAAQ,EAAG;AACnC,iBAAO,IAAI,KAAK,MAAM,OAAO,QAAQ,IAAI,YAAY,KAAK,YAAY,YAAY;AAAA,QACpF,CAAC;AACD,aAAK,GAAG,SAAS,CAAC,IAAI,SAAS;AAC7B,gBAAM,WAAW,UAAU,IAAI;AAC/B,cAAI,MAAM,IAAI,SAAS,QAAQ,EAAG;AAClC,cAAI,CAAC,KAAK,YAAY,IAAI,MAAM,QAAQ,QAAQ,CAAC;AAC/C,mBAAO,OAAO,IAAI,YAAY,YAAY;AAC5C,eAAK,YAAY,IAAI,QAAQ;AAC7B,iBAAO,IAAI,YAAY,EAAE;AAAA,QAC3B,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,UAAU,CAAC,QAAQ;AAAA,MACnB,OAAO;AAAA,MACP,YAAY,EAAE,eAAe,CAAC,aAAa,EAAE;AAAA,IAC/C;AAAA,IACA,CAAC,WAAW;AACV,UAAI;AAEJ,aAAO,GAAG,SAAS,MAAM,MAAS;AAClC,aAAO,GAAG,kBAAkB,CAAC,YAAY;AACvC,wBAAgB,SAAS,KAAK,CAAC,YAAY,QAAQ,aAAa,QAAQ,QAAQ;AAChF,qBAAa,OAAO;AAAA,MACtB,CAAC;AACD,aAAO,GAAG,SAAS,MAAM,iBAAiB,OAAO,QAAQ,aAAa,CAAC;AAAA,IACzE;AAAA,EACF;AACA,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAO,KAAK,SAAS,MAAM;AAC3B,WAAO,OAAO,QAAQ,QAAQ,GAAG,QAAQ,QAAQ,aAAa,OAAO;AAAA,EACvE,CAAC;AACD,QAAM,UAAU,OAAO,QAAQ;AAC/B,QAAM,iBAAiB,SAAS,CAAC;AACjC,QAAM,OAAO,CACX,MACA,OACA,QAAiE,CAAC,MAC/D;AACH,UAAM,OAAO,KAAK,MAAM,aAAa,WAAW,MAAM,WAAW,eAAe,QAAQ;AACxF,UAAM,WAAW,UAAU,IAAI;AAC/B,QAAI,CAAC,KAAK,YAAY,IAAI,MAAM,QAAQ,QAAQ,CAAC;AAC/C,YAAM,IAAI,MAAM,0BAA0B,MAAM,QAAQ,QAAQ,CAAC,EAAE;AACrE,SAAK,MAAM,IAAI,UAAU;AAAA,MACvB,OAAO,OAAO,KAAK,KAAK;AAAA,MACxB,MAAM,MAAM,QAAQ;AAAA,MACpB,OAAO,KAAK,MAAM,IAAI,IAAI,GAAI;AAAA,IAChC,CAAC;AAAA,EACH;AACA,QAAM,aAAa,MAAM,SAAS,QAAQ;AAC1C,MAAI,sBAAsB,SAAS,MAAM,QAAQ,UAAU;AACzD,UAAM,IAAI,MAAM,2BAA2B;AAC7C,SAAO;AAAA,IACL,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,eAAe,WAAW,aAAa;AAAA,IACvC,cAAc,SAAS,OAAO,QAAQ,CAAC,GAAG;AACxC,YAAM,YAAY,MAAM,aAAa;AACrC,YAAM,QAAQ,QAAQ,QAAQ,SAAS,SAAS,EAAE,MAAM,UAAU,OAAO;AACzE,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,YAAY,OAAO,EAAE;AACjD,YAAM,OAAO,UAAU,MAAM,QAAQ,WAAW,MAAM,YAAY,aAAa;AAC/E,WAAK,MAAM,OAAO,EAAE,UAAU,CAAC;AAC/B,YAAM,OAAO,KAAK,WAAW,eAAe,QAAQ,EAAE,MAAM,IAAI,IAAI;AACpE,WAAK,UAAU,MAAM;AACrB,aAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA,KAAK,QAAQ,CAAC,GAAG;AACf,YAAM,OAAO,KAAK,MAAM,aAAa,WAAW,MAAM,WAAW,eAAe,QAAQ;AACxF,aAAO,CAAC,GAAG,KAAK,KAAK,EAClB,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO;AAAA,QACtB;AAAA,QACA,MAAM,KAAK,MAAM;AAAA,QACjB,MAAM,KAAK;AAAA,QACX,OAAO,KAAK;AAAA,MACd,EAAE,EACD,KAAK,CAAC,MAAM,UAAU,KAAK,KAAK,cAAc,MAAM,IAAI,CAAC;AAAA,IAC9D;AAAA,IACA,MAAM,MAAM;AACV,kBAAY;AAAA,IACd;AAAA,IACA,SAAS,MAAM;AAAA,IACf,MAAM,WAAW;AACf,iBAAW,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC;AAChC,YAAI,cAAc,UAAa,IAAI,WAAW,GAAG,SAAS,IAAI,EAAG,OAAM,OAAO,GAAG;AACnF,iBAAW,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC;AACtC,YAAI,cAAc,UAAa,IAAI,WAAW,GAAG,SAAS,IAAI,EAAG,aAAY,OAAO,GAAG;AACzF,cAAQ,SAAS;AACjB,kBAAY;AAAA,IACd;AAAA,IACA,OAAO,MACL,IAAI;AAAA,MAAc,CAAC,SAAS,WAC1B,OAAO,MAAM,CAAC,UAAW,QAAQ,OAAO,KAAK,IAAI,QAAQ,CAAE;AAAA,IAC7D;AAAA,EACJ;AACF;",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@crvouga/mockingbird-service-aha",
3
+ "version": "0.1.0",
4
+ "description": "Stateful AHA phlebotomy mock with signed HTTP orders, status webhooks, and a real deterministic SSH/SFTP result server.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server.d.ts",
22
+ "default": "./dist/server.js"
23
+ },
24
+ "./sftp": {
25
+ "types": "./dist/sftp.d.ts",
26
+ "default": "./dist/sftp.js"
27
+ }
28
+ },
29
+ "bin": {
30
+ "mockingbird-aha": "./dist/cli.js"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/crvouga/mockingbird.git",
39
+ "directory": "packages/service/aha"
40
+ },
41
+ "homepage": "https://github.com/crvouga/mockingbird/tree/main/packages/service/aha#readme",
42
+ "keywords": [
43
+ "mockingbird",
44
+ "service",
45
+ "aha",
46
+ "phlebotomy",
47
+ "bloodwork",
48
+ "lab",
49
+ "webhooks",
50
+ "sftp"
51
+ ],
52
+ "mockingbird": {
53
+ "runtime": "portable",
54
+ "entries": {
55
+ "server": "node",
56
+ "cli": "node",
57
+ "sftp": "node"
58
+ },
59
+ "layer": "service",
60
+ "category": "health",
61
+ "displayName": "AHA",
62
+ "status": "wip"
63
+ },
64
+ "scripts": {
65
+ "generate": "bun ../../openapi/codegen/dist/cli.js",
66
+ "generate:check": "bun ../../openapi/codegen/dist/cli.js --check",
67
+ "openapi:check": "bun ../../openapi/codegen/dist/cli.js --validate",
68
+ "build": "bun ../../../scripts/bundle-service.ts",
69
+ "typecheck": "tsc -p tsconfig.json --noEmit",
70
+ "lint": "biome check .",
71
+ "test": "bun test",
72
+ "parity": "bun scripts/parity.ts",
73
+ "portability": "bun ../../../scripts/portability.ts",
74
+ "pack:check": "bun ../../../scripts/pack-check.ts"
75
+ },
76
+ "dependencies": {
77
+ "@crvouga/mockingbird-service-sqlite": "0.3.0",
78
+ "hono": "4.11.9",
79
+ "ssh2": "1.17.0"
80
+ },
81
+ "devDependencies": {
82
+ "@crvouga/mockingbird-adapter-node": "0.0.0-development",
83
+ "@crvouga/mockingbird-core": "0.0.0-development",
84
+ "@crvouga/mockingbird-http-codec": "0.0.0-development",
85
+ "@crvouga/mockingbird-openapi": "0.0.0-development",
86
+ "@crvouga/mockingbird-openapi-codegen": "0.1.0",
87
+ "@crvouga/mockingbird-openapi-metadata": "0.0.0-development",
88
+ "@crvouga/mockingbird-openbao": "0.0.0-development",
89
+ "@crvouga/mockingbird-parity": "0.0.0-development",
90
+ "@crvouga/mockingbird-service": "0.0.0-development",
91
+ "@crvouga/mockingbird-service-sqlite": "0.0.0-development",
92
+ "@crvouga/mockingbird-sqlite": "0.0.0-development",
93
+ "@crvouga/mockingbird-testing": "0.1.0",
94
+ "@types/ssh2": "1.15.6",
95
+ "fast-check": "4.9.0"
96
+ }
97
+ }