@emulates/aha 2.3.1 → 3.0.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/CHANGELOG.md +12 -2
- package/DISCOVERY.md +1 -1
- package/README.md +3 -3
- package/dist/{chunk-BEYOSACR.js → chunk-2KI57GGP.js} +55 -35
- package/dist/chunk-2KI57GGP.js.map +7 -0
- package/dist/{chunk-C6H2OGPE.js → chunk-5UIH5TAH.js} +3 -3
- package/dist/{chunk-C6H2OGPE.js.map → chunk-5UIH5TAH.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +5 -5
- package/dist/chunk-BEYOSACR.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# Changelog — @emulates/aha
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 3.0.0 (2026-10-07)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### ⚠️ Breaking changes
|
|
6
|
+
|
|
7
|
+
- point the repo at crvouga/emulates ([cdb5e53](https://github.com/crvouga/emulates/commit/cdb5e536ed8c1f52345e3984f089001c25fdb08a))
|
|
8
|
+
|
|
9
|
+
### Fixes and improvements
|
|
10
|
+
|
|
11
|
+
- format the emulates records query ([8698872](https://github.com/crvouga/emulates/commit/8698872509d06002be5a8fcdc85d79f8462320c3))
|
|
6
12
|
|
|
7
13
|
### Dependencies
|
|
8
14
|
|
|
9
15
|
- `@emulates/sqlite`
|
|
16
|
+
|
|
17
|
+
## 2.3.1 (2026-10-06)
|
|
18
|
+
|
|
19
|
+
Initial release.
|
package/DISCOVERY.md
CHANGED
|
@@ -47,7 +47,7 @@ designed for assertions and diagnosis by consuming test suites.
|
|
|
47
47
|
|
|
48
48
|
## Report a mismatch or missing capability
|
|
49
49
|
|
|
50
|
-
Follow the [agent reporting contract](https://github.com/crvouga/
|
|
50
|
+
Follow the [agent reporting contract](https://github.com/crvouga/emulates/blob/main/docs/REPORTING_ISSUES.md). Include package version,
|
|
51
51
|
operation/command, a minimal redacted request, actual emulator result, expected oracle result or vendor
|
|
52
52
|
documentation, and whether the mismatch appears in the matrix. Never include keys, tokens,
|
|
53
53
|
customer data, prompts, PHI, card data, or unredacted recordings.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @emulates/aha
|
|
2
2
|
|
|
3
|
-
> Part of [Emulates](https://github.com/crvouga/
|
|
3
|
+
> Part of [Emulates](https://github.com/crvouga/emulates): high-fidelity, in-process emulators for APIs and databases.
|
|
4
4
|
|
|
5
5
|
Stateful emulator of the **AHA (Advanced Health Academy) at-home phlebotomy** partner API for test
|
|
6
6
|
suites: HMAC-signed create-order and cancel, and — its main job — the order-status webhooks AHA
|
|
@@ -8,7 +8,7 @@ posts back. The vendor has no pull API, so every downstream effect (EMR appointm
|
|
|
8
8
|
storefront status, "blood drawn") starts with a webhook; the emulator emits one on demand, with every
|
|
9
9
|
field our handler reads, so the ZIP-routed bloodwork path can finally be tested.
|
|
10
10
|
|
|
11
|
-
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/
|
|
11
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/emulates/blob/main/packages/service/aha/SUPPORT.md)
|
|
12
12
|
- The vendor publishes no spec: the contract (`openapi.yaml`) is hand-authored from our
|
|
13
13
|
consumers' zod schemas and wire shapes.
|
|
14
14
|
|
|
@@ -175,4 +175,4 @@ Use `seed()` to install arbitrary binary fixtures or `publishResult(orderId, byt
|
|
|
175
175
|
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http` (autoSchedule ticks every 100 ms); the `serve` CLI target; port 8799. |
|
|
176
176
|
| `createAhaSftpServer` (`./sftp`) | Node | Real SSH/SFTP endpoint with deterministic host key/filesystem, shared order state, transfer controls, and an ephemeral port. |
|
|
177
177
|
|
|
178
|
-
Part of [
|
|
178
|
+
Part of [Emulates](https://github.com/crvouga/emulates).
|
|
@@ -33,7 +33,7 @@ var CORE_MIGRATIONS = [
|
|
|
33
33
|
{
|
|
34
34
|
id: "20260322_core_records_sequences",
|
|
35
35
|
sql: `
|
|
36
|
-
CREATE TABLE IF NOT EXISTS
|
|
36
|
+
CREATE TABLE IF NOT EXISTS emulates_records (
|
|
37
37
|
namespace TEXT NOT NULL,
|
|
38
38
|
collection TEXT NOT NULL,
|
|
39
39
|
id TEXT NOT NULL,
|
|
@@ -41,9 +41,9 @@ var CORE_MIGRATIONS = [
|
|
|
41
41
|
value TEXT NOT NULL,
|
|
42
42
|
PRIMARY KEY (namespace, collection, id)
|
|
43
43
|
);
|
|
44
|
-
CREATE INDEX IF NOT EXISTS
|
|
45
|
-
ON
|
|
46
|
-
CREATE TABLE IF NOT EXISTS
|
|
44
|
+
CREATE INDEX IF NOT EXISTS emulates_records_seq
|
|
45
|
+
ON emulates_records (namespace, collection, seq);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS emulates_sequences (
|
|
47
47
|
namespace TEXT NOT NULL,
|
|
48
48
|
name TEXT NOT NULL,
|
|
49
49
|
kind TEXT NOT NULL,
|
|
@@ -53,13 +53,33 @@ var CORE_MIGRATIONS = [
|
|
|
53
53
|
`
|
|
54
54
|
}
|
|
55
55
|
];
|
|
56
|
+
var LEGACY_TABLES = [
|
|
57
|
+
["emulators_records", "emulates_records"],
|
|
58
|
+
["emulators_sequences", "emulates_sequences"]
|
|
59
|
+
];
|
|
60
|
+
var LEGACY_INDEXES = [["emulators_records_seq", "emulates_records_seq"]];
|
|
61
|
+
var namesOf = (sqlite, type) => new Set(sqlite.prepare("SELECT name FROM sqlite_master WHERE type = ?").all(type).map((row) => row.name));
|
|
56
62
|
var migrateCore = (sqlite) => {
|
|
57
63
|
migrate(sqlite, CORE_MIGRATIONS);
|
|
64
|
+
const tables = namesOf(sqlite, "table");
|
|
65
|
+
const indexes = namesOf(sqlite, "index");
|
|
66
|
+
sqlite.transaction(() => {
|
|
67
|
+
for (const [from, to] of LEGACY_TABLES) {
|
|
68
|
+
if (tables.has(from) && !tables.has(to))
|
|
69
|
+
sqlite.exec(`ALTER TABLE ${from} RENAME TO ${to}`);
|
|
70
|
+
}
|
|
71
|
+
for (const [from, to] of LEGACY_INDEXES) {
|
|
72
|
+
if (!indexes.has(from) || indexes.has(to))
|
|
73
|
+
continue;
|
|
74
|
+
sqlite.exec(`DROP INDEX ${from}`);
|
|
75
|
+
sqlite.exec(`CREATE INDEX ${to} ON emulates_records (namespace, collection, seq)`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
58
78
|
};
|
|
59
79
|
var clearNamespace = (sqlite, namespace) => {
|
|
60
80
|
sqlite.transaction(() => {
|
|
61
|
-
sqlite.prepare("DELETE FROM
|
|
62
|
-
sqlite.prepare("DELETE FROM
|
|
81
|
+
sqlite.prepare("DELETE FROM emulates_records WHERE namespace = ?").run(namespace);
|
|
82
|
+
sqlite.prepare("DELETE FROM emulates_sequences WHERE namespace = ?").run(namespace);
|
|
63
83
|
});
|
|
64
84
|
};
|
|
65
85
|
|
|
@@ -74,9 +94,9 @@ var Collection = class {
|
|
|
74
94
|
this.collectionName = collectionName;
|
|
75
95
|
}
|
|
76
96
|
bumpCollectionSeq() {
|
|
77
|
-
const row = this.sqlite.prepare("SELECT value FROM
|
|
97
|
+
const row = this.sqlite.prepare("SELECT value FROM emulates_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.collectionName);
|
|
78
98
|
const next = (row?.value ?? 0) + 1;
|
|
79
|
-
this.sqlite.prepare(`INSERT INTO
|
|
99
|
+
this.sqlite.prepare(`INSERT INTO emulates_sequences (namespace, name, kind, value) VALUES (?, ?, 'collection', ?)
|
|
80
100
|
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.collectionName, next);
|
|
81
101
|
return next;
|
|
82
102
|
}
|
|
@@ -84,13 +104,13 @@ var Collection = class {
|
|
|
84
104
|
return this.sqlite.transaction(() => this.bumpCollectionSeq());
|
|
85
105
|
}
|
|
86
106
|
get(id) {
|
|
87
|
-
const row = this.sqlite.prepare("SELECT value FROM
|
|
107
|
+
const row = this.sqlite.prepare("SELECT value FROM emulates_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
88
108
|
if (!row)
|
|
89
109
|
return void 0;
|
|
90
110
|
return JSON.parse(row.value).value;
|
|
91
111
|
}
|
|
92
112
|
has(id) {
|
|
93
|
-
const row = this.sqlite.prepare("SELECT 1 AS ok FROM
|
|
113
|
+
const row = this.sqlite.prepare("SELECT 1 AS ok FROM emulates_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
94
114
|
return row !== void 0;
|
|
95
115
|
}
|
|
96
116
|
/** Insert a new record, assigning it the next sequence number. */
|
|
@@ -98,7 +118,7 @@ var Collection = class {
|
|
|
98
118
|
return this.sqlite.transaction(() => {
|
|
99
119
|
const seq = this.bumpCollectionSeq();
|
|
100
120
|
const stored = { seq, value };
|
|
101
|
-
this.sqlite.prepare(`INSERT INTO
|
|
121
|
+
this.sqlite.prepare(`INSERT INTO emulates_records (namespace, collection, id, seq, value)
|
|
102
122
|
VALUES (?, ?, ?, ?, ?)
|
|
103
123
|
ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.collectionName, id, seq, JSON.stringify(stored));
|
|
104
124
|
return stored;
|
|
@@ -107,25 +127,25 @@ var Collection = class {
|
|
|
107
127
|
/** Replace an existing record's value, keeping its position. */
|
|
108
128
|
update(id, value) {
|
|
109
129
|
return this.sqlite.transaction(() => {
|
|
110
|
-
const row = this.sqlite.prepare("SELECT seq, value FROM
|
|
130
|
+
const row = this.sqlite.prepare("SELECT seq, value FROM emulates_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
111
131
|
if (!row)
|
|
112
132
|
return void 0;
|
|
113
133
|
const stored = { seq: row.seq, value };
|
|
114
|
-
this.sqlite.prepare("UPDATE
|
|
134
|
+
this.sqlite.prepare("UPDATE emulates_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.collectionName, id);
|
|
115
135
|
return stored;
|
|
116
136
|
});
|
|
117
137
|
}
|
|
118
138
|
delete(id) {
|
|
119
|
-
const result = this.sqlite.prepare("DELETE FROM
|
|
139
|
+
const result = this.sqlite.prepare("DELETE FROM emulates_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.collectionName, id);
|
|
120
140
|
return result.changes > 0;
|
|
121
141
|
}
|
|
122
142
|
/** How many records the collection holds, without reading them. */
|
|
123
143
|
count() {
|
|
124
|
-
const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM
|
|
144
|
+
const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM emulates_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.collectionName);
|
|
125
145
|
return Number(row?.n ?? 0);
|
|
126
146
|
}
|
|
127
147
|
list(options = {}) {
|
|
128
|
-
const rows = this.sqlite.prepare("SELECT id, seq, value FROM
|
|
148
|
+
const rows = this.sqlite.prepare("SELECT id, seq, value FROM emulates_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.collectionName);
|
|
129
149
|
const out = [];
|
|
130
150
|
for (const row of rows) {
|
|
131
151
|
const stored = JSON.parse(row.value);
|
|
@@ -174,9 +194,9 @@ var IdSequence = class {
|
|
|
174
194
|
}
|
|
175
195
|
next(prefix, length = 14) {
|
|
176
196
|
return this.sqlite.transaction(() => {
|
|
177
|
-
const row = this.sqlite.prepare("SELECT value FROM
|
|
197
|
+
const row = this.sqlite.prepare("SELECT value FROM emulates_sequences WHERE namespace = ? AND name = ? AND kind = 'id'").get(this.namespace, prefix);
|
|
178
198
|
const value = (row?.value ?? 0) + 1;
|
|
179
|
-
this.sqlite.prepare(`INSERT INTO
|
|
199
|
+
this.sqlite.prepare(`INSERT INTO emulates_sequences (namespace, name, kind, value) VALUES (?, ?, 'id', ?)
|
|
180
200
|
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, prefix, value);
|
|
181
201
|
return `${prefix}${opaqueToken(`${this.salt}:${prefix}:${value}`, length)}`;
|
|
182
202
|
});
|
|
@@ -891,7 +911,7 @@ var serviceRoute = (template) => {
|
|
|
891
911
|
offset = match.index + match[0].length;
|
|
892
912
|
}
|
|
893
913
|
expression += escapePattern(segment.slice(offset));
|
|
894
|
-
return `:
|
|
914
|
+
return `:emulatesSegment${index}{${expression}}`;
|
|
895
915
|
}).join("/");
|
|
896
916
|
return {
|
|
897
917
|
path,
|
|
@@ -1034,7 +1054,7 @@ var json = (status, body) => new Response(JSON.stringify(body), {
|
|
|
1034
1054
|
status,
|
|
1035
1055
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1036
1056
|
});
|
|
1037
|
-
var adminError = (status, message) => json(status, { error: { type: "
|
|
1057
|
+
var adminError = (status, message) => json(status, { error: { type: "emulates_admin", message } });
|
|
1038
1058
|
var UNITS = {
|
|
1039
1059
|
ms: 1,
|
|
1040
1060
|
s: 1e3,
|
|
@@ -1986,18 +2006,18 @@ var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
|
1986
2006
|
// ../core/dist/snapshot.js
|
|
1987
2007
|
var snapshotNamespace = (sqlite, namespace) => ({
|
|
1988
2008
|
namespace,
|
|
1989
|
-
records: sqlite.prepare("SELECT collection, id, seq, value FROM
|
|
1990
|
-
sequences: sqlite.prepare("SELECT name, kind, value FROM
|
|
2009
|
+
records: sqlite.prepare("SELECT collection, id, seq, value FROM emulates_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
|
|
2010
|
+
sequences: sqlite.prepare("SELECT name, kind, value FROM emulates_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
|
|
1991
2011
|
});
|
|
1992
2012
|
var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
1993
2013
|
sqlite.transaction(() => {
|
|
1994
|
-
sqlite.prepare("DELETE FROM
|
|
1995
|
-
sqlite.prepare("DELETE FROM
|
|
1996
|
-
const record2 = sqlite.prepare("INSERT INTO
|
|
2014
|
+
sqlite.prepare("DELETE FROM emulates_records WHERE namespace = ?").run(namespace);
|
|
2015
|
+
sqlite.prepare("DELETE FROM emulates_sequences WHERE namespace = ?").run(namespace);
|
|
2016
|
+
const record2 = sqlite.prepare("INSERT INTO emulates_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
|
|
1997
2017
|
for (const row of snapshot.records) {
|
|
1998
2018
|
record2.run(namespace, row.collection, row.id, row.seq, row.value);
|
|
1999
2019
|
}
|
|
2000
|
-
const sequence = sqlite.prepare("INSERT INTO
|
|
2020
|
+
const sequence = sqlite.prepare("INSERT INTO emulates_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
|
|
2001
2021
|
for (const row of snapshot.sequences) {
|
|
2002
2022
|
sequence.run(namespace, row.name, row.kind, row.value);
|
|
2003
2023
|
}
|
|
@@ -2096,8 +2116,8 @@ var discoverCollections = (root) => {
|
|
|
2096
2116
|
return found;
|
|
2097
2117
|
};
|
|
2098
2118
|
var storedNames = (sqlite, namespace) => {
|
|
2099
|
-
const records = sqlite.prepare("SELECT DISTINCT collection AS name FROM
|
|
2100
|
-
const sequences = sqlite.prepare("SELECT DISTINCT name FROM
|
|
2119
|
+
const records = sqlite.prepare("SELECT DISTINCT collection AS name FROM emulates_records WHERE namespace = ?").all(namespace);
|
|
2120
|
+
const sequences = sqlite.prepare("SELECT DISTINCT name FROM emulates_sequences WHERE namespace = ? AND kind = 'collection'").all(namespace);
|
|
2101
2121
|
return [...records, ...sequences].map((row) => row.name);
|
|
2102
2122
|
};
|
|
2103
2123
|
var blankAccum = (name) => ({
|
|
@@ -2338,7 +2358,7 @@ var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
|
2338
2358
|
status,
|
|
2339
2359
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2340
2360
|
});
|
|
2341
|
-
var adminError2 = (status, message) => json3(status, { error: { type: "
|
|
2361
|
+
var adminError2 = (status, message) => json3(status, { error: { type: "emulates_admin", message } });
|
|
2342
2362
|
var fromResult = (result, status) => result.ok ? json3(status, result.value) : adminError2(result.status, result.message);
|
|
2343
2363
|
var integerParam = (value) => {
|
|
2344
2364
|
if (value === null)
|
|
@@ -2439,7 +2459,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2439
2459
|
};
|
|
2440
2460
|
|
|
2441
2461
|
// ../core/dist/version.js
|
|
2442
|
-
var PACKAGE_VERSION = true ? "
|
|
2462
|
+
var PACKAGE_VERSION = true ? "3.0.0" : UNRELEASED_VERSION;
|
|
2443
2463
|
|
|
2444
2464
|
// ../core/dist/signing.js
|
|
2445
2465
|
var encoder = new TextEncoder();
|
|
@@ -2824,7 +2844,7 @@ var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
|
2824
2844
|
status,
|
|
2825
2845
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2826
2846
|
});
|
|
2827
|
-
var adminError3 = (status, message) => json4(status, { error: { type: "
|
|
2847
|
+
var adminError3 = (status, message) => json4(status, { error: { type: "emulates_admin", message } });
|
|
2828
2848
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2829
2849
|
var parseEndpoint = (value) => {
|
|
2830
2850
|
if (!isRecord4(value) || typeof value.url !== "string")
|
|
@@ -3428,7 +3448,7 @@ var createRuntime = (options) => {
|
|
|
3428
3448
|
log(400);
|
|
3429
3449
|
return stamp(new Response(JSON.stringify({
|
|
3430
3450
|
error: {
|
|
3431
|
-
type: "
|
|
3451
|
+
type: "emulates_admin",
|
|
3432
3452
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
3433
3453
|
}
|
|
3434
3454
|
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
@@ -3594,7 +3614,7 @@ var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
|
3594
3614
|
status,
|
|
3595
3615
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
3596
3616
|
});
|
|
3597
|
-
var adminFail = (status, message) => adminJson(status, { error: { type: "
|
|
3617
|
+
var adminFail = (status, message) => adminJson(status, { error: { type: "emulates_admin", message } });
|
|
3598
3618
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3599
3619
|
var credentialRoutes = (registry) => ({
|
|
3600
3620
|
"GET /credentials": () => adminJson(200, {
|
|
@@ -3848,7 +3868,7 @@ var AHA_PRESETS = {
|
|
|
3848
3868
|
}
|
|
3849
3869
|
};
|
|
3850
3870
|
var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
3851
|
-
var adminError4 = (status, message) => json5(status, { error: { type: "
|
|
3871
|
+
var adminError4 = (status, message) => json5(status, { error: { type: "emulates_admin", message } });
|
|
3852
3872
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3853
3873
|
var parseAutoSchedule = (value) => {
|
|
3854
3874
|
if (value === null) return null;
|
|
@@ -4349,4 +4369,4 @@ export {
|
|
|
4349
4369
|
apiKeyCredential,
|
|
4350
4370
|
AhaAPI
|
|
4351
4371
|
};
|
|
4352
|
-
//# sourceMappingURL=chunk-
|
|
4372
|
+
//# sourceMappingURL=chunk-2KI57GGP.js.map
|