@crvouga/mockingbird-service-junction 3.5.0 → 3.6.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 +11 -1
- package/README.md +46 -0
- package/SUPPORT.md +1 -1
- package/dist/{chunk-Y477MTXC.js → chunk-WMJJYHRV.js} +232 -42
- package/dist/chunk-WMJJYHRV.js.map +7 -0
- package/dist/{chunk-KMCP67GK.js → chunk-ZRF3DDJ4.js} +294 -175
- package/dist/{chunk-KMCP67GK.js.map → chunk-ZRF3DDJ4.js.map} +4 -4
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +18 -1
- package/dist/server.js +2 -2
- package/openapi.yaml +20 -1
- package/package.json +1 -1
- package/dist/chunk-Y477MTXC.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-junction
|
|
2
2
|
|
|
3
|
-
## 3.
|
|
3
|
+
## 3.6.0 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- resolve fleet and emulator issue batch ([26d2f8c](https://github.com/crvouga/mockingbird/commit/26d2f8cee99fefed93192204bac44fb1be35c1da))
|
|
8
|
+
|
|
9
|
+
### Fixes and improvements
|
|
10
|
+
|
|
11
|
+
- fix profile example and smoke sequencing ([ba49f2d](https://github.com/crvouga/mockingbird/commit/ba49f2d8d33c7431db3ebda3f86f2efed955cd5d))
|
|
12
|
+
|
|
13
|
+
## 3.5.0 (2026-10-15)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
16
|
|
package/README.md
CHANGED
|
@@ -910,3 +910,49 @@ The USSL testkit result metadata follows the observed sandbox fixture (null spec
|
|
|
910
910
|
and CLIA `05D2130115`); other laboratories retain their existing metadata behavior.
|
|
911
911
|
|
|
912
912
|
Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [README](https://github.com/crvouga/mockingbird#readme) · [llms.txt](https://github.com/crvouga/mockingbird/blob/main/llms.txt) · [report an issue or request a feature](https://github.com/crvouga/mockingbird/blob/main/docs/REPORTING_ISSUES.md).
|
|
913
|
+
|
|
914
|
+
### Lab-account behavior profiles
|
|
915
|
+
|
|
916
|
+
`createRuntime({ labAccountBehavior: { profile, accounts } })` selects a named policy.
|
|
917
|
+
`native` is the backward-compatible default. `expected-contract` enforces the
|
|
918
|
+
[documented lab/account association and billing rules](https://docs.junction.com/lab/overview/lab-accounts),
|
|
919
|
+
including rejecting a selected account belonging to another lab. This is an assumption
|
|
920
|
+
from the public contract, **not measured production parity**. Both `expected-contract`
|
|
921
|
+
and `sandbox-observed` return the persisted account association in POST/GET; a synthetic
|
|
922
|
+
requisition uses that same stored provider account number.
|
|
923
|
+
|
|
924
|
+
`sandbox-observed` uses explicit, account-owned observations, never a global delegated
|
|
925
|
+
account billing override. For example, with synthetic fixture IDs:
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
import { createRuntime } from "@crvouga/mockingbird-service-junction"
|
|
929
|
+
|
|
930
|
+
const runtime = createRuntime({
|
|
931
|
+
labAccountBehavior: {
|
|
932
|
+
profile: "sandbox-observed",
|
|
933
|
+
accounts: {
|
|
934
|
+
"synthetic-delegated-id": {
|
|
935
|
+
rejectBilling: { client_bill: "Lab 13 does not support billing type patient_bill_passthrough" },
|
|
936
|
+
},
|
|
937
|
+
"synthetic-ny-id": { allowBilling: { client_bill: ["NY", "NJ"] } },
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
These controls encode the independently reported sandbox observations in #440/#441;
|
|
944
|
+
they do not establish production billing behavior. Exceptions apply only to explicit
|
|
945
|
+
root account IDs and explicit billing. `defaultBillingTypes` still governs omitted
|
|
946
|
+
billing independently. `GET/PUT /__admin/lab-account-behavior` inspect/change one
|
|
947
|
+
namespace's policy; changes survive its reset and never affect another namespace.
|
|
948
|
+
Unknown profile names are rejected. CLI/server equivalents are `--lab-account-profile`
|
|
949
|
+
and `--lab-account-sandbox-policies <json-file>` (the account-policy object).
|
|
950
|
+
|
|
951
|
+
The explicit profiles also accept `order_set.add_on` with `provider_ids` or `marker_ids`
|
|
952
|
+
from one lab's configured marker catalog. The selected markers determine the synthetic
|
|
953
|
+
custom panel; unresolved or cross-lab combinations refuse without placing an order.
|
|
954
|
+
Combining `lab_test_ids` and `add_on` in one request is explicitly refused by these profiles
|
|
955
|
+
until the combined-panel semantics are verified. Malformed add-ons also refuse without placing
|
|
956
|
+
an order; requested add-on markers are never silently discarded.
|
|
957
|
+
Panel and custom orders share account/billing checks for home and walk-in collection.
|
|
958
|
+
The native profile keeps its existing panel-only contract.
|
package/SUPPORT.md
CHANGED
|
@@ -23,7 +23,7 @@ Generated from `openapi.yaml`; do not edit by hand.
|
|
|
23
23
|
| `get_labs_v3_lab_tests_labs_get` | `GET /v3/lab_tests/labs` | ✅ supported | ✅ | |
|
|
24
24
|
| `get_markers_for_lab_test_v3_lab_tests__lab_test_id__markers_get` | `GET /v3/lab_tests/{lab_test_id}/markers` | ✅ supported | ✅ | |
|
|
25
25
|
| `list_order_set_markers_v3_lab_tests_list_order_set_markers_post` | `POST /v3/lab_tests/list_order_set_markers` | ✅ supported | ✅ | |
|
|
26
|
-
| `create_order_v3_order_post` | `POST /v3/order` | ✅ supported | ✅ |
|
|
26
|
+
| `create_order_v3_order_post` | `POST /v3/order` | ✅ supported | ✅ | Native profile parity only; explicit account profiles add fixture-owned billing policies and custom-marker orders, without production parity certification. |
|
|
27
27
|
| `get_area_info_v3_order_area_info_get` | `GET /v3/order/area/info` | ✅ supported | ❌ disabled | central_labs presence, within_radius counts and getlabs market coverage are provider-owned geo data that an emulator cannot replicate per-zip; covered by the client-parity scenario shape checks instead |
|
|
28
28
|
| `get_psc_info_v3_order_psc_info_get` | `GET /v3/order/psc/info` | ✅ supported | ❌ disabled | patient_service_centers are real site inventories per zip (thousands of sites, result-capped at 30) that a fixture emulator cannot replicate per-zip; covered by the client-parity scenario shape checks instead |
|
|
29
29
|
| `get_phlebotomy_appointment_availability_v3_order_phlebotomy_appointment_availability_post` | `POST /v3/order/phlebotomy/appointment/availability` | ✅ supported | ❌ disabled | availability slots are provider-side data whose values shift between sandbox calls; covered by the emulator-internal scheduling property suite instead |
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createClock,
|
|
4
4
|
createRuntime,
|
|
5
5
|
defaultBillingTypesFrom,
|
|
6
|
+
labAccountBehaviorFrom,
|
|
6
7
|
normalizeBootControls,
|
|
7
8
|
normalizeControls,
|
|
8
9
|
ownedTransport,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
pullCorpus,
|
|
12
13
|
resolveAdminPrefix,
|
|
13
14
|
restoreClock
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-ZRF3DDJ4.js";
|
|
15
16
|
import {
|
|
16
17
|
defaultCorpus
|
|
17
18
|
} from "./chunk-2FZJPMTR.js";
|
|
@@ -24,9 +25,6 @@ import { readFile as readFile6 } from "node:fs/promises";
|
|
|
24
25
|
|
|
25
26
|
// ../../adapters/node/dist/cli.js
|
|
26
27
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
27
|
-
import { createRequire as createRequire3 } from "node:module";
|
|
28
|
-
import { resolve as resolve3 } from "node:path";
|
|
29
|
-
import { pathToFileURL } from "node:url";
|
|
30
28
|
import { parseArgs } from "node:util";
|
|
31
29
|
|
|
32
30
|
// ../../adapters/node/dist/fleet.js
|
|
@@ -40,6 +38,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
|
|
|
40
38
|
|
|
41
39
|
// ../../adapters/node/dist/fleet-archive.js
|
|
42
40
|
import { createHash } from "node:crypto";
|
|
41
|
+
function validateCompatibilityTags(tags) {
|
|
42
|
+
if (!tags || typeof tags !== "object" || Array.isArray(tags))
|
|
43
|
+
throw new Error("invalid archive compatibility tags");
|
|
44
|
+
const entries = Object.entries(tags);
|
|
45
|
+
if (entries.length > 32 || entries.some(([key, value]) => !/^[A-Za-z0-9_.-]{1,64}$/.test(key) || typeof value !== "string" || value.length < 1 || value.length > 256 || [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)))
|
|
46
|
+
throw new Error("invalid archive compatibility tags");
|
|
47
|
+
}
|
|
48
|
+
function assertCompatibilityTags(tags, policy) {
|
|
49
|
+
if (tags !== void 0)
|
|
50
|
+
validateCompatibilityTags(tags);
|
|
51
|
+
if (!policy)
|
|
52
|
+
return;
|
|
53
|
+
validateCompatibilityTags(policy.tags);
|
|
54
|
+
if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
|
|
55
|
+
throw new Error("invalid missing compatibility policy");
|
|
56
|
+
if (tags === void 0 && policy.missing === "allow")
|
|
57
|
+
return;
|
|
58
|
+
if (canonical(tags) !== canonical(policy.tags))
|
|
59
|
+
throw new Error("application archive compatibility mismatch");
|
|
60
|
+
}
|
|
61
|
+
function assertArchiveCompatibility(input, policy) {
|
|
62
|
+
const archive = validateArchive(input);
|
|
63
|
+
assertCompatibilityTags(archive.compatibility, policy);
|
|
64
|
+
return archive;
|
|
65
|
+
}
|
|
43
66
|
var canonical = (value) => {
|
|
44
67
|
if (Array.isArray(value))
|
|
45
68
|
return `[${value.map(canonical).join(",")}]`;
|
|
@@ -59,6 +82,8 @@ function validateArchive(value) {
|
|
|
59
82
|
const { sha256, ...content } = doc;
|
|
60
83
|
if (doc.format !== "mockingbird.fleet/v1" || typeof doc.sourceId !== "string" || typeof doc.createdAt !== "string" || !doc.namespaces || typeof doc.namespaces !== "object" || Array.isArray(doc.namespaces) || sha256 !== digest(content))
|
|
61
84
|
throw new Error("unsupported or corrupt fleet archive");
|
|
85
|
+
if (doc.compatibility !== void 0)
|
|
86
|
+
validateCompatibilityTags(doc.compatibility);
|
|
62
87
|
for (const [namespace, entry] of Object.entries(doc.namespaces)) {
|
|
63
88
|
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace) || !entry || !Number.isFinite(entry.clock?.now) || typeof entry.clock?.frozen !== "boolean" || !entry.services || !Object.keys(entry.services).length)
|
|
64
89
|
throw new Error("invalid archive namespace");
|
|
@@ -74,6 +99,7 @@ function inspectArchive(input) {
|
|
|
74
99
|
format: archive.format,
|
|
75
100
|
createdAt: archive.createdAt,
|
|
76
101
|
sha256: archive.sha256,
|
|
102
|
+
...archive.compatibility ? { compatibility: archive.compatibility } : {},
|
|
77
103
|
namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name, ns]) => [
|
|
78
104
|
name,
|
|
79
105
|
{
|
|
@@ -716,11 +742,28 @@ var ingressSupport = {
|
|
|
716
742
|
streaming: true,
|
|
717
743
|
signing: "vendor request signing is not used; webhook signatures remain payload based"
|
|
718
744
|
},
|
|
745
|
+
cognito: {
|
|
746
|
+
publicOrigin: true,
|
|
747
|
+
pathPrefix: true,
|
|
748
|
+
namespaces: "path-or-header",
|
|
749
|
+
streaming: true,
|
|
750
|
+
signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
|
|
751
|
+
},
|
|
752
|
+
medplum: {
|
|
753
|
+
publicOrigin: true,
|
|
754
|
+
pathPrefix: true,
|
|
755
|
+
namespaces: "path-or-header",
|
|
756
|
+
streaming: true,
|
|
757
|
+
signing: "FHIR, OAuth and storage URLs use the configured public namespace"
|
|
758
|
+
},
|
|
719
759
|
other: {
|
|
720
760
|
publicOrigin: false,
|
|
721
761
|
reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
|
|
722
762
|
}
|
|
723
763
|
};
|
|
764
|
+
function supportsPublicIngress(service) {
|
|
765
|
+
return service === "stripe" || service === "cognito" || service === "medplum";
|
|
766
|
+
}
|
|
724
767
|
function advertisedHttpBase(entry) {
|
|
725
768
|
if (entry.publicPath !== void 0 && !entry.publicOrigin)
|
|
726
769
|
throw new Error("publicPath requires publicOrigin");
|
|
@@ -1259,7 +1302,7 @@ data: ${JSON.stringify(data)}
|
|
|
1259
1302
|
{
|
|
1260
1303
|
protocol: c.protocol,
|
|
1261
1304
|
coverage: c.coverage,
|
|
1262
|
-
ingress: c.targetName
|
|
1305
|
+
ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
|
|
1263
1306
|
target: c.targetName,
|
|
1264
1307
|
outboundPolicy: c.outboundPolicy ?? "unsupported",
|
|
1265
1308
|
version: 1,
|
|
@@ -2343,11 +2386,12 @@ data: ${JSON.stringify(data)}
|
|
|
2343
2386
|
format: "mockingbird.fleet/v1",
|
|
2344
2387
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2345
2388
|
sourceId: this.ctx.id,
|
|
2389
|
+
...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
|
|
2346
2390
|
namespaces: contents
|
|
2347
2391
|
});
|
|
2348
2392
|
}
|
|
2349
2393
|
async restoreArchive(input, selectedNamespace) {
|
|
2350
|
-
const archive =
|
|
2394
|
+
const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
|
|
2351
2395
|
const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
|
|
2352
2396
|
if (!entries.length)
|
|
2353
2397
|
throw new Error("selected namespace absent from archive");
|
|
@@ -2931,7 +2975,7 @@ async function preflightFleet(config, options) {
|
|
|
2931
2975
|
if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
|
|
2932
2976
|
throw new Error("target must name a supported service suffix");
|
|
2933
2977
|
const service = entry.target ?? name;
|
|
2934
|
-
if (advertisedHttpBase(entry) && service
|
|
2978
|
+
if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
|
|
2935
2979
|
throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
|
|
2936
2980
|
const target = await options.load(service);
|
|
2937
2981
|
const protocol = "protocol" in target ? target.protocol : "http";
|
|
@@ -2982,7 +3026,7 @@ async function preflightFleet(config, options) {
|
|
|
2982
3026
|
break;
|
|
2983
3027
|
}
|
|
2984
3028
|
}
|
|
2985
|
-
targets.set(name, { ...target, packageVersion: packageVersion ?? "3.
|
|
3029
|
+
targets.set(name, { ...target, packageVersion: packageVersion ?? "3.6.0" });
|
|
2986
3030
|
} catch (error) {
|
|
2987
3031
|
throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2988
3032
|
}
|
|
@@ -3026,6 +3070,11 @@ async function preflightFleet(config, options) {
|
|
|
3026
3070
|
return ordered;
|
|
3027
3071
|
}
|
|
3028
3072
|
function validateFleetPolicies(options) {
|
|
3073
|
+
if (options.archiveCompatibility) {
|
|
3074
|
+
validateCompatibilityTags(options.archiveCompatibility.tags);
|
|
3075
|
+
if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
|
|
3076
|
+
throw new Error("archiveCompatibility.missing must be allow or reject");
|
|
3077
|
+
}
|
|
3029
3078
|
if (options.journal && !options.repository)
|
|
3030
3079
|
throw new Error("durable operation journal requires an archive repository");
|
|
3031
3080
|
if (options.cors) {
|
|
@@ -4018,10 +4067,44 @@ async function startFleet(config, options) {
|
|
|
4018
4067
|
}
|
|
4019
4068
|
}
|
|
4020
4069
|
|
|
4021
|
-
// ../../adapters/node/dist/fleet-
|
|
4022
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
4070
|
+
// ../../adapters/node/dist/fleet-loader.js
|
|
4023
4071
|
import { createRequire as createRequire2 } from "node:module";
|
|
4024
4072
|
import { resolve as resolve2 } from "node:path";
|
|
4073
|
+
import { pathToFileURL } from "node:url";
|
|
4074
|
+
var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4075
|
+
function createInstalledServiceLoader(options) {
|
|
4076
|
+
if (!options.projectRoot)
|
|
4077
|
+
throw new Error("consumer projectRoot required");
|
|
4078
|
+
const allowed = new Set(options.services);
|
|
4079
|
+
for (const name of allowed)
|
|
4080
|
+
if (!suffix.test(name))
|
|
4081
|
+
throw new Error("invalid service suffix");
|
|
4082
|
+
const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
|
|
4083
|
+
const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
|
|
4084
|
+
return async (name) => {
|
|
4085
|
+
if (!suffix.test(name) || !allowed.has(name))
|
|
4086
|
+
throw new Error("service is not declared in loader allowlist");
|
|
4087
|
+
const own = targets.get(name);
|
|
4088
|
+
if (own)
|
|
4089
|
+
return own;
|
|
4090
|
+
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
4091
|
+
let location;
|
|
4092
|
+
try {
|
|
4093
|
+
location = require2.resolve(specifier);
|
|
4094
|
+
} catch {
|
|
4095
|
+
throw new Error(`cannot load service "${name}": install @crvouga/mockingbird-service-${name} in the consumer project; its /server export is required`);
|
|
4096
|
+
}
|
|
4097
|
+
const mod = await import(pathToFileURL(location).href);
|
|
4098
|
+
if (!mod.serveTarget || mod.serveTarget.name !== name || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
|
|
4099
|
+
throw new Error(`${specifier} exports no matching serveTarget`);
|
|
4100
|
+
return mod.serveTarget;
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
// ../../adapters/node/dist/fleet-tools.js
|
|
4105
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
4106
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
4107
|
+
import { resolve as resolve3 } from "node:path";
|
|
4025
4108
|
|
|
4026
4109
|
// ../../adapters/node/dist/fleet-client.js
|
|
4027
4110
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
@@ -4035,6 +4118,14 @@ var FleetClientError = class extends Error {
|
|
|
4035
4118
|
this.details = details;
|
|
4036
4119
|
}
|
|
4037
4120
|
};
|
|
4121
|
+
var FleetClientResponseError = class extends FleetClientError {
|
|
4122
|
+
code;
|
|
4123
|
+
constructor(code, status, origin) {
|
|
4124
|
+
super(status, { code, body: "omitted", ...origin ? { origin } : {} });
|
|
4125
|
+
this.code = code;
|
|
4126
|
+
this.name = "FleetClientResponseError";
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4038
4129
|
var FleetClient = class {
|
|
4039
4130
|
base;
|
|
4040
4131
|
options;
|
|
@@ -4047,7 +4138,14 @@ var FleetClient = class {
|
|
|
4047
4138
|
if (url.username || url.password)
|
|
4048
4139
|
throw new Error("fleet client URL must not contain credentials");
|
|
4049
4140
|
url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
|
|
4141
|
+
const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
|
|
4142
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
4143
|
+
throw new Error("invalid response byte limit");
|
|
4144
|
+
const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
|
|
4145
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
4146
|
+
signal.throwIfAborted();
|
|
4050
4147
|
const response = await (this.options.fetch ?? fetch)(url, {
|
|
4148
|
+
redirect: "manual",
|
|
4051
4149
|
method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
|
|
4052
4150
|
headers: {
|
|
4053
4151
|
"content-type": "application/json",
|
|
@@ -4055,11 +4153,71 @@ var FleetClient = class {
|
|
|
4055
4153
|
...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
|
|
4056
4154
|
},
|
|
4057
4155
|
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
|
|
4058
|
-
signal
|
|
4156
|
+
signal
|
|
4059
4157
|
});
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
throw new
|
|
4158
|
+
if (response.status >= 300 && response.status < 400) {
|
|
4159
|
+
await response.body?.cancel();
|
|
4160
|
+
throw new FleetClientResponseError("redirect", response.status, url.origin);
|
|
4161
|
+
}
|
|
4162
|
+
if (Number(response.headers.get("content-length")) > limit) {
|
|
4163
|
+
await response.body?.cancel();
|
|
4164
|
+
throw new FleetClientResponseError("response_too_large", response.status);
|
|
4165
|
+
}
|
|
4166
|
+
const reader = response.body?.getReader();
|
|
4167
|
+
const chunks = [];
|
|
4168
|
+
let size = 0;
|
|
4169
|
+
const abort = () => {
|
|
4170
|
+
void reader?.cancel(signal.reason).catch(() => {
|
|
4171
|
+
});
|
|
4172
|
+
};
|
|
4173
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4174
|
+
try {
|
|
4175
|
+
if (reader)
|
|
4176
|
+
while (true) {
|
|
4177
|
+
signal.throwIfAborted();
|
|
4178
|
+
const { done, value } = await reader.read();
|
|
4179
|
+
signal.throwIfAborted();
|
|
4180
|
+
if (done)
|
|
4181
|
+
break;
|
|
4182
|
+
size += value.byteLength;
|
|
4183
|
+
if (size > limit) {
|
|
4184
|
+
await reader.cancel();
|
|
4185
|
+
throw new FleetClientResponseError("response_too_large", response.status);
|
|
4186
|
+
}
|
|
4187
|
+
chunks.push(value);
|
|
4188
|
+
}
|
|
4189
|
+
} finally {
|
|
4190
|
+
signal.removeEventListener("abort", abort);
|
|
4191
|
+
if (signal.aborted)
|
|
4192
|
+
await reader?.cancel(signal.reason).catch(() => {
|
|
4193
|
+
});
|
|
4194
|
+
reader?.releaseLock();
|
|
4195
|
+
}
|
|
4196
|
+
const bytes = new Uint8Array(size);
|
|
4197
|
+
let offset = 0;
|
|
4198
|
+
for (const chunk of chunks) {
|
|
4199
|
+
bytes.set(chunk, offset);
|
|
4200
|
+
offset += chunk.byteLength;
|
|
4201
|
+
}
|
|
4202
|
+
const text = new TextDecoder().decode(bytes);
|
|
4203
|
+
let data;
|
|
4204
|
+
try {
|
|
4205
|
+
data = text.length ? JSON.parse(text) : void 0;
|
|
4206
|
+
} catch {
|
|
4207
|
+
if (response.ok)
|
|
4208
|
+
throw new FleetClientResponseError("invalid_json", response.status);
|
|
4209
|
+
}
|
|
4210
|
+
if (!response.ok) {
|
|
4211
|
+
const error = data && typeof data === "object" ? data.error : void 0;
|
|
4212
|
+
const detail = { body: "omitted" };
|
|
4213
|
+
if (error && typeof error === "object")
|
|
4214
|
+
for (const key of ["type", "code"]) {
|
|
4215
|
+
const value = error[key];
|
|
4216
|
+
if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
|
|
4217
|
+
detail[key] = value;
|
|
4218
|
+
}
|
|
4219
|
+
throw new FleetClientError(response.status, detail);
|
|
4220
|
+
}
|
|
4063
4221
|
return data;
|
|
4064
4222
|
}
|
|
4065
4223
|
discover() {
|
|
@@ -4125,17 +4283,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
|
|
|
4125
4283
|
const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
|
|
4126
4284
|
if (!child || !secret)
|
|
4127
4285
|
throw new Error(`missing service mapping ${entry.service}`);
|
|
4286
|
+
let endpoint;
|
|
4128
4287
|
if (child.protocol === "http") {
|
|
4129
4288
|
const template = child.namespaces.path;
|
|
4130
4289
|
if (typeof template !== "string")
|
|
4131
4290
|
throw new Error(`missing HTTP namespace mapping ${entry.service}`);
|
|
4132
|
-
|
|
4291
|
+
endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
|
|
4133
4292
|
} else {
|
|
4134
|
-
const
|
|
4135
|
-
if (!
|
|
4293
|
+
const selected = secret.namespaces?.[namespace];
|
|
4294
|
+
if (!selected)
|
|
4136
4295
|
throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
|
|
4137
|
-
|
|
4296
|
+
endpoint = selected;
|
|
4138
4297
|
}
|
|
4298
|
+
const url = new URL(endpoint);
|
|
4299
|
+
const field = entry.field ?? "url";
|
|
4300
|
+
let value;
|
|
4301
|
+
switch (field) {
|
|
4302
|
+
case "url":
|
|
4303
|
+
value = endpoint;
|
|
4304
|
+
break;
|
|
4305
|
+
case "hostname":
|
|
4306
|
+
value = url.hostname.replace(/^\[|\]$/g, "");
|
|
4307
|
+
break;
|
|
4308
|
+
case "port":
|
|
4309
|
+
value = url.port;
|
|
4310
|
+
break;
|
|
4311
|
+
case "username":
|
|
4312
|
+
value = decodeURIComponent(url.username);
|
|
4313
|
+
break;
|
|
4314
|
+
case "password":
|
|
4315
|
+
value = decodeURIComponent(url.password);
|
|
4316
|
+
break;
|
|
4317
|
+
case "pathname":
|
|
4318
|
+
value = decodeURIComponent(url.pathname);
|
|
4319
|
+
break;
|
|
4320
|
+
case "database":
|
|
4321
|
+
value = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
4322
|
+
break;
|
|
4323
|
+
default:
|
|
4324
|
+
throw new Error("unsupported connection field");
|
|
4325
|
+
}
|
|
4326
|
+
if (!value) {
|
|
4327
|
+
if (typeof entry.fallback !== "string")
|
|
4328
|
+
throw new Error(`missing connection component ${field}`);
|
|
4329
|
+
value = entry.fallback;
|
|
4330
|
+
}
|
|
4331
|
+
result[variable] = value;
|
|
4139
4332
|
}
|
|
4140
4333
|
return result;
|
|
4141
4334
|
}
|
|
@@ -4159,7 +4352,7 @@ async function doctorFleet(config, options, probes = []) {
|
|
|
4159
4352
|
const plan = [];
|
|
4160
4353
|
try {
|
|
4161
4354
|
const targets = await preflightFleet(config, options);
|
|
4162
|
-
const require2 =
|
|
4355
|
+
const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
|
|
4163
4356
|
for (const [service, target] of targets) {
|
|
4164
4357
|
let version = "development";
|
|
4165
4358
|
try {
|
|
@@ -4214,7 +4407,7 @@ async function doctorFleet(config, options, probes = []) {
|
|
|
4214
4407
|
};
|
|
4215
4408
|
}
|
|
4216
4409
|
async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
|
|
4217
|
-
const archive =
|
|
4410
|
+
const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
|
|
4218
4411
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4219
4412
|
const isolated = {
|
|
4220
4413
|
...config,
|
|
@@ -4226,7 +4419,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
|
|
|
4226
4419
|
};
|
|
4227
4420
|
let fleet;
|
|
4228
4421
|
try {
|
|
4229
|
-
fleet = await startFleet(isolated,
|
|
4422
|
+
fleet = await startFleet(isolated, options);
|
|
4230
4423
|
const client = new FleetClient(fleet.manifest.adminBase, {
|
|
4231
4424
|
...config.adminKey ? { adminKey: config.adminKey } : {}
|
|
4232
4425
|
});
|
|
@@ -4601,26 +4794,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
|
|
|
4601
4794
|
normalizeBootControls(document, "adminControls");
|
|
4602
4795
|
return document;
|
|
4603
4796
|
};
|
|
4604
|
-
var loadTarget =
|
|
4605
|
-
if (name === own.name)
|
|
4606
|
-
return own;
|
|
4607
|
-
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
4608
|
-
try {
|
|
4609
|
-
let mod;
|
|
4610
|
-
try {
|
|
4611
|
-
mod = await import(specifier);
|
|
4612
|
-
} catch {
|
|
4613
|
-
const fromProject = createRequire3(resolve3(process.cwd(), "package.json"));
|
|
4614
|
-
mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
|
|
4615
|
-
}
|
|
4616
|
-
if (!mod.serveTarget)
|
|
4617
|
-
throw new Error(`${specifier} exports no serveTarget`);
|
|
4618
|
-
return mod.serveTarget;
|
|
4619
|
-
} catch (error) {
|
|
4620
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4621
|
-
throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
|
|
4622
|
-
}
|
|
4623
|
-
};
|
|
4797
|
+
var loadTarget = (name, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name], targets: [own] })(name);
|
|
4624
4798
|
var start = async (target, values, config) => {
|
|
4625
4799
|
const adminPrefix = resolveAdminPrefix(config.adminPrefix);
|
|
4626
4800
|
const runtime = await target.create(values, {
|
|
@@ -4813,6 +4987,16 @@ var serveTarget = {
|
|
|
4813
4987
|
description: "Unknown user ids: 404 (strict) or create on first use",
|
|
4814
4988
|
default: "strict"
|
|
4815
4989
|
},
|
|
4990
|
+
"lab-account-profile": {
|
|
4991
|
+
type: "string",
|
|
4992
|
+
value: "<native|expected-contract|sandbox-observed>",
|
|
4993
|
+
description: "Lab account policy (default native; production behavior is assumed, not certified)"
|
|
4994
|
+
},
|
|
4995
|
+
"lab-account-sandbox-policies": {
|
|
4996
|
+
type: "string",
|
|
4997
|
+
value: "<file>",
|
|
4998
|
+
description: "Account id to explicit billing sandbox observations (JSON object)"
|
|
4999
|
+
},
|
|
4816
5000
|
"default-billing-type": {
|
|
4817
5001
|
type: "string",
|
|
4818
5002
|
value: "<lab=type,\u2026>",
|
|
@@ -4887,6 +5071,11 @@ var serveTarget = {
|
|
|
4887
5071
|
if (identity !== void 0 && !IDENTITY_MODES.includes(identity)) {
|
|
4888
5072
|
throw new Error(`--identity must be one of ${IDENTITY_MODES.join(", ")} (got ${identity})`);
|
|
4889
5073
|
}
|
|
5074
|
+
const policyFile = asString2(values["lab-account-sandbox-policies"]);
|
|
5075
|
+
const labAccountBehavior = labAccountBehaviorFrom({
|
|
5076
|
+
profile: asString2(values["lab-account-profile"]) ?? "native",
|
|
5077
|
+
accounts: policyFile ? await readJson(policyFile) : {}
|
|
5078
|
+
});
|
|
4890
5079
|
const defaultBillingTypes = billingDefaults(asString2(values["default-billing-type"]));
|
|
4891
5080
|
const webhookUrl = asString2(values["webhook-url"]);
|
|
4892
5081
|
const webhookSecret = asString2(values["webhook-secret"]) ?? process.env.MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET;
|
|
@@ -4903,6 +5092,7 @@ var serveTarget = {
|
|
|
4903
5092
|
...teamId !== void 0 ? { teamId } : {},
|
|
4904
5093
|
...maxUsers !== void 0 ? { limits: { maxUsers } } : {},
|
|
4905
5094
|
...identity !== void 0 ? { identity } : {},
|
|
5095
|
+
labAccountBehavior,
|
|
4906
5096
|
...defaultBillingTypes ? { defaultBillingTypes } : {},
|
|
4907
5097
|
...journalSize !== void 0 ? { journalSize } : {},
|
|
4908
5098
|
...webhookUrl !== void 0 && webhookSecret !== void 0 ? {
|
|
@@ -4945,4 +5135,4 @@ export {
|
|
|
4945
5135
|
createServer2 as createServer,
|
|
4946
5136
|
serveTarget
|
|
4947
5137
|
};
|
|
4948
|
-
//# sourceMappingURL=chunk-
|
|
5138
|
+
//# sourceMappingURL=chunk-WMJJYHRV.js.map
|