@crvouga/mockingbird-service-docker 1.5.0 → 1.7.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 +13 -1
- package/dist/{chunk-DG4KWFV5.js → chunk-2VTSZWES.js} +2 -2
- package/dist/{chunk-PEU54Q4H.js → chunk-7DDFNYJE.js} +115 -31
- package/dist/chunk-7DDFNYJE.js.map +7 -0
- package/dist/cli.js +315 -51
- package/dist/cli.js.map +4 -4
- package/dist/index.d.ts +35 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +76 -0
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-PEU54Q4H.js.map +0 -7
- /package/dist/{chunk-DG4KWFV5.js.map → chunk-2VTSZWES.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
serveTarget
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2VTSZWES.js";
|
|
5
5
|
import {
|
|
6
6
|
createClock,
|
|
7
7
|
normalizeBootControls,
|
|
8
8
|
normalizeControls,
|
|
9
|
+
normalizeIdBase,
|
|
9
10
|
ownedTransport,
|
|
10
11
|
parseDuration,
|
|
11
12
|
parseFaultHeader,
|
|
12
13
|
resolveAdminPrefix,
|
|
13
14
|
restoreClock
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-7DDFNYJE.js";
|
|
15
16
|
|
|
16
17
|
// ../../adapters/node/dist/cli.js
|
|
17
18
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
18
|
-
import { createRequire as createRequire3 } from "node:module";
|
|
19
|
-
import { resolve as resolve3 } from "node:path";
|
|
20
|
-
import { pathToFileURL } from "node:url";
|
|
21
19
|
import { parseArgs } from "node:util";
|
|
22
20
|
|
|
21
|
+
// ../../adapters/node/dist/boot-settings.js
|
|
22
|
+
var checkSettings = (target, settings, label) => {
|
|
23
|
+
if (!target.parseSettings)
|
|
24
|
+
throw new Error(`${label}: ${target.name} takes no boot settings`);
|
|
25
|
+
try {
|
|
26
|
+
return target.parseSettings(settings);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`${label}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
29
|
+
cause: error
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
23
34
|
// ../../adapters/node/dist/fleet.js
|
|
24
35
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
25
36
|
import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
|
|
@@ -31,6 +42,31 @@ import { createHash as createHash3, randomBytes, randomUUID as randomUUID3 } fro
|
|
|
31
42
|
|
|
32
43
|
// ../../adapters/node/dist/fleet-archive.js
|
|
33
44
|
import { createHash } from "node:crypto";
|
|
45
|
+
function validateCompatibilityTags(tags) {
|
|
46
|
+
if (!tags || typeof tags !== "object" || Array.isArray(tags))
|
|
47
|
+
throw new Error("invalid archive compatibility tags");
|
|
48
|
+
const entries = Object.entries(tags);
|
|
49
|
+
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)))
|
|
50
|
+
throw new Error("invalid archive compatibility tags");
|
|
51
|
+
}
|
|
52
|
+
function assertCompatibilityTags(tags, policy) {
|
|
53
|
+
if (tags !== void 0)
|
|
54
|
+
validateCompatibilityTags(tags);
|
|
55
|
+
if (!policy)
|
|
56
|
+
return;
|
|
57
|
+
validateCompatibilityTags(policy.tags);
|
|
58
|
+
if (policy.missing !== void 0 && !["allow", "reject"].includes(policy.missing))
|
|
59
|
+
throw new Error("invalid missing compatibility policy");
|
|
60
|
+
if (tags === void 0 && policy.missing === "allow")
|
|
61
|
+
return;
|
|
62
|
+
if (canonical(tags) !== canonical(policy.tags))
|
|
63
|
+
throw new Error("application archive compatibility mismatch");
|
|
64
|
+
}
|
|
65
|
+
function assertArchiveCompatibility(input, policy) {
|
|
66
|
+
const archive = validateArchive(input);
|
|
67
|
+
assertCompatibilityTags(archive.compatibility, policy);
|
|
68
|
+
return archive;
|
|
69
|
+
}
|
|
34
70
|
var canonical = (value) => {
|
|
35
71
|
if (Array.isArray(value))
|
|
36
72
|
return `[${value.map(canonical).join(",")}]`;
|
|
@@ -50,6 +86,8 @@ function validateArchive(value) {
|
|
|
50
86
|
const { sha256, ...content } = doc;
|
|
51
87
|
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))
|
|
52
88
|
throw new Error("unsupported or corrupt fleet archive");
|
|
89
|
+
if (doc.compatibility !== void 0)
|
|
90
|
+
validateCompatibilityTags(doc.compatibility);
|
|
53
91
|
for (const [namespace, entry] of Object.entries(doc.namespaces)) {
|
|
54
92
|
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)
|
|
55
93
|
throw new Error("invalid archive namespace");
|
|
@@ -65,6 +103,7 @@ function inspectArchive(input) {
|
|
|
65
103
|
format: archive.format,
|
|
66
104
|
createdAt: archive.createdAt,
|
|
67
105
|
sha256: archive.sha256,
|
|
106
|
+
...archive.compatibility ? { compatibility: archive.compatibility } : {},
|
|
68
107
|
namespaces: Object.fromEntries(Object.entries(archive.namespaces).map(([name, ns]) => [
|
|
69
108
|
name,
|
|
70
109
|
{
|
|
@@ -707,11 +746,28 @@ var ingressSupport = {
|
|
|
707
746
|
streaming: true,
|
|
708
747
|
signing: "vendor request signing is not used; webhook signatures remain payload based"
|
|
709
748
|
},
|
|
749
|
+
cognito: {
|
|
750
|
+
publicOrigin: true,
|
|
751
|
+
pathPrefix: true,
|
|
752
|
+
namespaces: "path-or-header",
|
|
753
|
+
streaming: true,
|
|
754
|
+
signing: "JWT issuer and JWKS use the configured public namespace; SigV4 verification is not modelled"
|
|
755
|
+
},
|
|
756
|
+
medplum: {
|
|
757
|
+
publicOrigin: true,
|
|
758
|
+
pathPrefix: true,
|
|
759
|
+
namespaces: "path-or-header",
|
|
760
|
+
streaming: true,
|
|
761
|
+
signing: "FHIR, OAuth and storage URLs use the configured public namespace"
|
|
762
|
+
},
|
|
710
763
|
other: {
|
|
711
764
|
publicOrigin: false,
|
|
712
765
|
reason: "host/path-sensitive signing and hosted URL rewriting are not certified; use a direct endpoint"
|
|
713
766
|
}
|
|
714
767
|
};
|
|
768
|
+
function supportsPublicIngress(service) {
|
|
769
|
+
return service === "stripe" || service === "cognito" || service === "medplum";
|
|
770
|
+
}
|
|
715
771
|
function advertisedHttpBase(entry) {
|
|
716
772
|
if (entry.publicPath !== void 0 && !entry.publicOrigin)
|
|
717
773
|
throw new Error("publicPath requires publicOrigin");
|
|
@@ -1250,7 +1306,7 @@ data: ${JSON.stringify(data)}
|
|
|
1250
1306
|
{
|
|
1251
1307
|
protocol: c.protocol,
|
|
1252
1308
|
coverage: c.coverage,
|
|
1253
|
-
ingress: c.targetName
|
|
1309
|
+
ingress: supportsPublicIngress(c.targetName ?? "") ? ingressSupport[c.targetName] : ingressSupport.other,
|
|
1254
1310
|
target: c.targetName,
|
|
1255
1311
|
outboundPolicy: c.outboundPolicy ?? "unsupported",
|
|
1256
1312
|
version: 1,
|
|
@@ -2334,11 +2390,12 @@ data: ${JSON.stringify(data)}
|
|
|
2334
2390
|
format: "mockingbird.fleet/v1",
|
|
2335
2391
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2336
2392
|
sourceId: this.ctx.id,
|
|
2393
|
+
...this.ctx.options.archiveCompatibility ? { compatibility: structuredClone(this.ctx.options.archiveCompatibility.tags) } : {},
|
|
2337
2394
|
namespaces: contents
|
|
2338
2395
|
});
|
|
2339
2396
|
}
|
|
2340
2397
|
async restoreArchive(input, selectedNamespace) {
|
|
2341
|
-
const archive =
|
|
2398
|
+
const archive = assertArchiveCompatibility(input, this.ctx.options.archiveCompatibility);
|
|
2342
2399
|
const entries = Object.entries(archive.namespaces).filter(([ns]) => selectedNamespace === void 0 || ns === selectedNamespace);
|
|
2343
2400
|
if (!entries.length)
|
|
2344
2401
|
throw new Error("selected namespace absent from archive");
|
|
@@ -2623,6 +2680,9 @@ var fleet_config_schema_default = {
|
|
|
2623
2680
|
adminControls: {
|
|
2624
2681
|
$ref: "#/$defs/controls"
|
|
2625
2682
|
},
|
|
2683
|
+
idBase: {
|
|
2684
|
+
$ref: "#/$defs/idBase"
|
|
2685
|
+
},
|
|
2626
2686
|
namespace: {
|
|
2627
2687
|
type: "string",
|
|
2628
2688
|
pattern: "^[A-Za-z0-9_.-]{1,64}$"
|
|
@@ -2712,6 +2772,12 @@ var fleet_config_schema_default = {
|
|
|
2712
2772
|
adminControls: {
|
|
2713
2773
|
$ref: "#/$defs/controls"
|
|
2714
2774
|
},
|
|
2775
|
+
idBase: {
|
|
2776
|
+
$ref: "#/$defs/idBase"
|
|
2777
|
+
},
|
|
2778
|
+
settings: {
|
|
2779
|
+
type: "object"
|
|
2780
|
+
},
|
|
2715
2781
|
fixtures: {
|
|
2716
2782
|
type: "object"
|
|
2717
2783
|
},
|
|
@@ -2758,6 +2824,18 @@ var fleet_config_schema_default = {
|
|
|
2758
2824
|
}
|
|
2759
2825
|
}
|
|
2760
2826
|
},
|
|
2827
|
+
idBase: {
|
|
2828
|
+
anyOf: [
|
|
2829
|
+
{
|
|
2830
|
+
type: "integer",
|
|
2831
|
+
minimum: 0
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
type: "string",
|
|
2835
|
+
pattern: "^(boot|[0-9]+)$"
|
|
2836
|
+
}
|
|
2837
|
+
]
|
|
2838
|
+
},
|
|
2761
2839
|
controls: {
|
|
2762
2840
|
type: "object",
|
|
2763
2841
|
additionalProperties: false,
|
|
@@ -2907,6 +2985,8 @@ async function preflightFleet(config, options) {
|
|
|
2907
2985
|
throw new Error("protected archive policy requires a protected repository; configure its protector before startup");
|
|
2908
2986
|
resolveAdminPrefix(config.adminPrefix);
|
|
2909
2987
|
normalizeBootControls(config.adminControls);
|
|
2988
|
+
if (config.idBase !== void 0)
|
|
2989
|
+
normalizeIdBase(config.idBase, "idBase");
|
|
2910
2990
|
const targets = /* @__PURE__ */ new Map();
|
|
2911
2991
|
const ports = /* @__PURE__ */ new Set();
|
|
2912
2992
|
for (const [name, entry] of Object.entries(config.services)) {
|
|
@@ -2922,7 +3002,7 @@ async function preflightFleet(config, options) {
|
|
|
2922
3002
|
if (entry.target !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.target))
|
|
2923
3003
|
throw new Error("target must name a supported service suffix");
|
|
2924
3004
|
const service = entry.target ?? name;
|
|
2925
|
-
if (advertisedHttpBase(entry) && service
|
|
3005
|
+
if (advertisedHttpBase(entry) && !supportsPublicIngress(service))
|
|
2926
3006
|
throw new Error(`public ingress unsupported for ${service}; use a direct endpoint to preserve signed targets`);
|
|
2927
3007
|
const target = await options.load(service);
|
|
2928
3008
|
const protocol = "protocol" in target ? target.protocol : "http";
|
|
@@ -2938,6 +3018,13 @@ async function preflightFleet(config, options) {
|
|
|
2938
3018
|
throw new Error("invalid protocol namespace selector");
|
|
2939
3019
|
}
|
|
2940
3020
|
}
|
|
3021
|
+
if (entry.idBase !== void 0)
|
|
3022
|
+
normalizeIdBase(entry.idBase, "idBase");
|
|
3023
|
+
if (entry.settings !== void 0) {
|
|
3024
|
+
if (protocol !== "http" || !("create" in target))
|
|
3025
|
+
throw new Error("settings: only HTTP services take boot settings");
|
|
3026
|
+
checkSettings(target, entry.settings, "settings");
|
|
3027
|
+
}
|
|
2941
3028
|
const port = entry.port ?? target.defaultPort;
|
|
2942
3029
|
if (port !== 0) {
|
|
2943
3030
|
const key = `${entry.host ?? "127.0.0.1"}:${port}`;
|
|
@@ -2973,7 +3060,7 @@ async function preflightFleet(config, options) {
|
|
|
2973
3060
|
break;
|
|
2974
3061
|
}
|
|
2975
3062
|
}
|
|
2976
|
-
targets.set(name, { ...target, packageVersion: packageVersion ?? "1.
|
|
3063
|
+
targets.set(name, { ...target, packageVersion: packageVersion ?? "1.7.0" });
|
|
2977
3064
|
} catch (error) {
|
|
2978
3065
|
throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2979
3066
|
}
|
|
@@ -3017,6 +3104,11 @@ async function preflightFleet(config, options) {
|
|
|
3017
3104
|
return ordered;
|
|
3018
3105
|
}
|
|
3019
3106
|
function validateFleetPolicies(options) {
|
|
3107
|
+
if (options.archiveCompatibility) {
|
|
3108
|
+
validateCompatibilityTags(options.archiveCompatibility.tags);
|
|
3109
|
+
if (options.archiveCompatibility.missing !== void 0 && !["allow", "reject"].includes(options.archiveCompatibility.missing))
|
|
3110
|
+
throw new Error("archiveCompatibility.missing must be allow or reject");
|
|
3111
|
+
}
|
|
3020
3112
|
if (options.journal && !options.repository)
|
|
3021
3113
|
throw new Error("durable operation journal requires an archive repository");
|
|
3022
3114
|
if (options.cors) {
|
|
@@ -3288,6 +3380,7 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
|
|
|
3288
3380
|
options.onLog?.(entry2);
|
|
3289
3381
|
},
|
|
3290
3382
|
...entry.adminControls !== void 0 ? { adminControls: entry.adminControls } : {},
|
|
3383
|
+
...entry.settings !== void 0 ? { settings: checkSettings(target, entry.settings, "settings") } : {},
|
|
3291
3384
|
...entry.fixtures !== void 0 ? { fixtures: entry.fixtures } : {},
|
|
3292
3385
|
...(advertisedHttpBase(entry) ?? entry.baseUrl) !== void 0 ? { baseUrl: advertisedHttpBase(entry) ?? entry.baseUrl } : {}
|
|
3293
3386
|
});
|
|
@@ -3306,6 +3399,8 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
|
|
|
3306
3399
|
].some((method) => typeof created[method] !== "function"))
|
|
3307
3400
|
throw new Error("HTTP target does not implement the fleet runtime protocol; build/install matching supported service versions");
|
|
3308
3401
|
const runtime = created;
|
|
3402
|
+
if (entry.idBase !== void 0)
|
|
3403
|
+
runtime.setIdBase?.(entry.idBase);
|
|
3309
3404
|
const unobserve = runtime.webhooks?.observeDeliveries(observer.delivery);
|
|
3310
3405
|
if (options.outbound && runtime.webhooks)
|
|
3311
3406
|
runtime.webhooks.setTransportPolicy(ownedTransport(options.outbound));
|
|
@@ -3904,6 +3999,7 @@ async function startFleet(config, options) {
|
|
|
3904
3999
|
...entry,
|
|
3905
4000
|
adminPrefix: resolveAdminPrefix(entry.adminPrefix ?? adminPrefix),
|
|
3906
4001
|
...adminKey !== void 0 ? { adminKey } : {},
|
|
4002
|
+
...(entry.idBase ?? config.idBase) !== void 0 ? { idBase: entry.idBase ?? config.idBase } : {},
|
|
3907
4003
|
// Fleet-wide controls, with the service's own keys replacing them.
|
|
3908
4004
|
...config.adminControls !== void 0 || entry.adminControls !== void 0 ? { adminControls: { ...config.adminControls, ...entry.adminControls } } : {}
|
|
3909
4005
|
}, options, (namespace, request) => management?.admit(namespace, request), (namespace) => management?.generation(namespace), (namespace, request) => management?.privateRequest(namespace, request) ?? false, {
|
|
@@ -4009,10 +4105,44 @@ async function startFleet(config, options) {
|
|
|
4009
4105
|
}
|
|
4010
4106
|
}
|
|
4011
4107
|
|
|
4012
|
-
// ../../adapters/node/dist/fleet-
|
|
4013
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
4108
|
+
// ../../adapters/node/dist/fleet-loader.js
|
|
4014
4109
|
import { createRequire as createRequire2 } from "node:module";
|
|
4015
4110
|
import { resolve as resolve2 } from "node:path";
|
|
4111
|
+
import { pathToFileURL } from "node:url";
|
|
4112
|
+
var suffix = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4113
|
+
function createInstalledServiceLoader(options) {
|
|
4114
|
+
if (!options.projectRoot)
|
|
4115
|
+
throw new Error("consumer projectRoot required");
|
|
4116
|
+
const allowed = new Set(options.services);
|
|
4117
|
+
for (const name of allowed)
|
|
4118
|
+
if (!suffix.test(name))
|
|
4119
|
+
throw new Error("invalid service suffix");
|
|
4120
|
+
const require2 = createRequire2(resolve2(options.projectRoot, "package.json"));
|
|
4121
|
+
const targets = new Map((options.targets ?? []).map((target) => [target.name, target]));
|
|
4122
|
+
return async (name) => {
|
|
4123
|
+
if (!suffix.test(name) || !allowed.has(name))
|
|
4124
|
+
throw new Error("service is not declared in loader allowlist");
|
|
4125
|
+
const own = targets.get(name);
|
|
4126
|
+
if (own)
|
|
4127
|
+
return own;
|
|
4128
|
+
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
4129
|
+
let location;
|
|
4130
|
+
try {
|
|
4131
|
+
location = require2.resolve(specifier);
|
|
4132
|
+
} catch {
|
|
4133
|
+
throw new Error(`cannot load service "${name}": install @crvouga/mockingbird-service-${name} in the consumer project; its /server export is required`);
|
|
4134
|
+
}
|
|
4135
|
+
const mod = await import(pathToFileURL(location).href);
|
|
4136
|
+
if (!mod.serveTarget || mod.serveTarget.name !== name || ("protocol" in mod.serveTarget ? typeof mod.serveTarget.start !== "function" : typeof mod.serveTarget.create !== "function"))
|
|
4137
|
+
throw new Error(`${specifier} exports no matching serveTarget`);
|
|
4138
|
+
return mod.serveTarget;
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// ../../adapters/node/dist/fleet-tools.js
|
|
4143
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
4144
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
4145
|
+
import { resolve as resolve3 } from "node:path";
|
|
4016
4146
|
|
|
4017
4147
|
// ../../adapters/node/dist/fleet-client.js
|
|
4018
4148
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
@@ -4026,6 +4156,14 @@ var FleetClientError = class extends Error {
|
|
|
4026
4156
|
this.details = details;
|
|
4027
4157
|
}
|
|
4028
4158
|
};
|
|
4159
|
+
var FleetClientResponseError = class extends FleetClientError {
|
|
4160
|
+
code;
|
|
4161
|
+
constructor(code2, status, origin) {
|
|
4162
|
+
super(status, { code: code2, body: "omitted", ...origin ? { origin } : {} });
|
|
4163
|
+
this.code = code2;
|
|
4164
|
+
this.name = "FleetClientResponseError";
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4029
4167
|
var FleetClient = class {
|
|
4030
4168
|
base;
|
|
4031
4169
|
options;
|
|
@@ -4038,7 +4176,14 @@ var FleetClient = class {
|
|
|
4038
4176
|
if (url.username || url.password)
|
|
4039
4177
|
throw new Error("fleet client URL must not contain credentials");
|
|
4040
4178
|
url.searchParams.set("namespace", options.namespace ?? this.options.namespace ?? "default");
|
|
4179
|
+
const limit = options.maxResponseBytes ?? this.options.maxResponseBytes ?? 64 * 1024 * 1024;
|
|
4180
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
4181
|
+
throw new Error("invalid response byte limit");
|
|
4182
|
+
const deadline = AbortSignal.timeout(this.options.timeoutMs ?? 3e4);
|
|
4183
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
4184
|
+
signal.throwIfAborted();
|
|
4041
4185
|
const response = await (this.options.fetch ?? fetch)(url, {
|
|
4186
|
+
redirect: "manual",
|
|
4042
4187
|
method: options.method ?? (options.body === void 0 ? "GET" : "POST"),
|
|
4043
4188
|
headers: {
|
|
4044
4189
|
"content-type": "application/json",
|
|
@@ -4046,11 +4191,71 @@ var FleetClient = class {
|
|
|
4046
4191
|
...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
|
|
4047
4192
|
},
|
|
4048
4193
|
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
|
|
4049
|
-
signal
|
|
4194
|
+
signal
|
|
4050
4195
|
});
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
throw new
|
|
4196
|
+
if (response.status >= 300 && response.status < 400) {
|
|
4197
|
+
await response.body?.cancel();
|
|
4198
|
+
throw new FleetClientResponseError("redirect", response.status, url.origin);
|
|
4199
|
+
}
|
|
4200
|
+
if (Number(response.headers.get("content-length")) > limit) {
|
|
4201
|
+
await response.body?.cancel();
|
|
4202
|
+
throw new FleetClientResponseError("response_too_large", response.status);
|
|
4203
|
+
}
|
|
4204
|
+
const reader = response.body?.getReader();
|
|
4205
|
+
const chunks = [];
|
|
4206
|
+
let size = 0;
|
|
4207
|
+
const abort = () => {
|
|
4208
|
+
void reader?.cancel(signal.reason).catch(() => {
|
|
4209
|
+
});
|
|
4210
|
+
};
|
|
4211
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4212
|
+
try {
|
|
4213
|
+
if (reader)
|
|
4214
|
+
while (true) {
|
|
4215
|
+
signal.throwIfAborted();
|
|
4216
|
+
const { done, value } = await reader.read();
|
|
4217
|
+
signal.throwIfAborted();
|
|
4218
|
+
if (done)
|
|
4219
|
+
break;
|
|
4220
|
+
size += value.byteLength;
|
|
4221
|
+
if (size > limit) {
|
|
4222
|
+
await reader.cancel();
|
|
4223
|
+
throw new FleetClientResponseError("response_too_large", response.status);
|
|
4224
|
+
}
|
|
4225
|
+
chunks.push(value);
|
|
4226
|
+
}
|
|
4227
|
+
} finally {
|
|
4228
|
+
signal.removeEventListener("abort", abort);
|
|
4229
|
+
if (signal.aborted)
|
|
4230
|
+
await reader?.cancel(signal.reason).catch(() => {
|
|
4231
|
+
});
|
|
4232
|
+
reader?.releaseLock();
|
|
4233
|
+
}
|
|
4234
|
+
const bytes = new Uint8Array(size);
|
|
4235
|
+
let offset = 0;
|
|
4236
|
+
for (const chunk of chunks) {
|
|
4237
|
+
bytes.set(chunk, offset);
|
|
4238
|
+
offset += chunk.byteLength;
|
|
4239
|
+
}
|
|
4240
|
+
const text = new TextDecoder().decode(bytes);
|
|
4241
|
+
let data;
|
|
4242
|
+
try {
|
|
4243
|
+
data = text.length ? JSON.parse(text) : void 0;
|
|
4244
|
+
} catch {
|
|
4245
|
+
if (response.ok)
|
|
4246
|
+
throw new FleetClientResponseError("invalid_json", response.status);
|
|
4247
|
+
}
|
|
4248
|
+
if (!response.ok) {
|
|
4249
|
+
const error = data && typeof data === "object" ? data.error : void 0;
|
|
4250
|
+
const detail = { body: "omitted" };
|
|
4251
|
+
if (error && typeof error === "object")
|
|
4252
|
+
for (const key of ["type", "code"]) {
|
|
4253
|
+
const value = error[key];
|
|
4254
|
+
if (typeof value === "string" && /^[A-Za-z0-9_.-]{1,128}$/.test(value))
|
|
4255
|
+
detail[key] = value;
|
|
4256
|
+
}
|
|
4257
|
+
throw new FleetClientError(response.status, detail);
|
|
4258
|
+
}
|
|
4054
4259
|
return data;
|
|
4055
4260
|
}
|
|
4056
4261
|
discover() {
|
|
@@ -4116,17 +4321,52 @@ function renderConnections(manifest, privateManifest, namespace, mapping) {
|
|
|
4116
4321
|
const child = manifest.services[entry.service], secret = privateManifest.services[entry.service];
|
|
4117
4322
|
if (!child || !secret)
|
|
4118
4323
|
throw new Error(`missing service mapping ${entry.service}`);
|
|
4324
|
+
let endpoint;
|
|
4119
4325
|
if (child.protocol === "http") {
|
|
4120
4326
|
const template = child.namespaces.path;
|
|
4121
4327
|
if (typeof template !== "string")
|
|
4122
4328
|
throw new Error(`missing HTTP namespace mapping ${entry.service}`);
|
|
4123
|
-
|
|
4329
|
+
endpoint = new URL(template.replace("{name}", encodeURIComponent(namespace)), secret.url).href;
|
|
4124
4330
|
} else {
|
|
4125
|
-
const
|
|
4126
|
-
if (!
|
|
4331
|
+
const selected = secret.namespaces?.[namespace];
|
|
4332
|
+
if (!selected)
|
|
4127
4333
|
throw new Error(`missing namespace mapping ${namespace}/${entry.service}`);
|
|
4128
|
-
|
|
4334
|
+
endpoint = selected;
|
|
4335
|
+
}
|
|
4336
|
+
const url = new URL(endpoint);
|
|
4337
|
+
const field = entry.field ?? "url";
|
|
4338
|
+
let value;
|
|
4339
|
+
switch (field) {
|
|
4340
|
+
case "url":
|
|
4341
|
+
value = endpoint;
|
|
4342
|
+
break;
|
|
4343
|
+
case "hostname":
|
|
4344
|
+
value = url.hostname.replace(/^\[|\]$/g, "");
|
|
4345
|
+
break;
|
|
4346
|
+
case "port":
|
|
4347
|
+
value = url.port;
|
|
4348
|
+
break;
|
|
4349
|
+
case "username":
|
|
4350
|
+
value = decodeURIComponent(url.username);
|
|
4351
|
+
break;
|
|
4352
|
+
case "password":
|
|
4353
|
+
value = decodeURIComponent(url.password);
|
|
4354
|
+
break;
|
|
4355
|
+
case "pathname":
|
|
4356
|
+
value = decodeURIComponent(url.pathname);
|
|
4357
|
+
break;
|
|
4358
|
+
case "database":
|
|
4359
|
+
value = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
4360
|
+
break;
|
|
4361
|
+
default:
|
|
4362
|
+
throw new Error("unsupported connection field");
|
|
4129
4363
|
}
|
|
4364
|
+
if (!value) {
|
|
4365
|
+
if (typeof entry.fallback !== "string")
|
|
4366
|
+
throw new Error(`missing connection component ${field}`);
|
|
4367
|
+
value = entry.fallback;
|
|
4368
|
+
}
|
|
4369
|
+
result[variable] = value;
|
|
4130
4370
|
}
|
|
4131
4371
|
return result;
|
|
4132
4372
|
}
|
|
@@ -4150,7 +4390,7 @@ async function doctorFleet(config, options, probes = []) {
|
|
|
4150
4390
|
const plan = [];
|
|
4151
4391
|
try {
|
|
4152
4392
|
const targets = await preflightFleet(config, options);
|
|
4153
|
-
const require2 =
|
|
4393
|
+
const require2 = createRequire3(resolve3(process.cwd(), "package.json"));
|
|
4154
4394
|
for (const [service, target] of targets) {
|
|
4155
4395
|
let version = "development";
|
|
4156
4396
|
try {
|
|
@@ -4205,7 +4445,7 @@ async function doctorFleet(config, options, probes = []) {
|
|
|
4205
4445
|
};
|
|
4206
4446
|
}
|
|
4207
4447
|
async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e4) {
|
|
4208
|
-
const archive =
|
|
4448
|
+
const archive = assertArchiveCompatibility(input, options.archiveCompatibility);
|
|
4209
4449
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4210
4450
|
const isolated = {
|
|
4211
4451
|
...config,
|
|
@@ -4217,7 +4457,7 @@ async function rehearseFleet(config, options, input, probes = [], timeoutMs = 3e
|
|
|
4217
4457
|
};
|
|
4218
4458
|
let fleet;
|
|
4219
4459
|
try {
|
|
4220
|
-
fleet = await startFleet(isolated,
|
|
4460
|
+
fleet = await startFleet(isolated, options);
|
|
4221
4461
|
const client = new FleetClient(fleet.manifest.adminBase, {
|
|
4222
4462
|
...config.adminKey ? { adminKey: config.adminKey } : {}
|
|
4223
4463
|
});
|
|
@@ -4444,6 +4684,16 @@ var COMMON_SERVE_OPTIONS = {
|
|
|
4444
4684
|
value: "<seed>",
|
|
4445
4685
|
description: "Seed for every random choice (env MOCKINGBIRD_SEED)"
|
|
4446
4686
|
},
|
|
4687
|
+
"id-base": {
|
|
4688
|
+
type: "string",
|
|
4689
|
+
value: "<n|boot>",
|
|
4690
|
+
description: "Start generated ids at this base so a restart does not reissue the last boot's ids; boot derives one from the clock (env MOCKINGBIRD_ID_BASE; default 0, the same ids every boot)"
|
|
4691
|
+
},
|
|
4692
|
+
settings: {
|
|
4693
|
+
type: "string",
|
|
4694
|
+
value: "<json|file>",
|
|
4695
|
+
description: "Service settings to boot with, as JSON or a JSON file; every namespace starts from them (env MOCKINGBIRD_SETTINGS; only for services that declare settings)"
|
|
4696
|
+
},
|
|
4447
4697
|
"admin-controls": {
|
|
4448
4698
|
type: "string",
|
|
4449
4699
|
value: "<json|file>",
|
|
@@ -4538,19 +4788,40 @@ var readJsonFile = async (path, label) => {
|
|
|
4538
4788
|
}
|
|
4539
4789
|
};
|
|
4540
4790
|
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4791
|
+
var readJsonOption = async (source, label) => {
|
|
4792
|
+
const trimmed = source.trim();
|
|
4793
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
4794
|
+
try {
|
|
4795
|
+
return JSON.parse(trimmed);
|
|
4796
|
+
} catch {
|
|
4797
|
+
throw new Error(`${label}: not valid JSON`);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
return readJsonFile(source, label);
|
|
4801
|
+
};
|
|
4802
|
+
var resolveServeIdBase = (values, env = process.env) => {
|
|
4803
|
+
const source = asString(values["id-base"]) ?? env.MOCKINGBIRD_ID_BASE;
|
|
4804
|
+
if (source === void 0 || source.trim() === "")
|
|
4805
|
+
return void 0;
|
|
4806
|
+
normalizeIdBase(source.trim(), "--id-base");
|
|
4807
|
+
return source.trim();
|
|
4808
|
+
};
|
|
4809
|
+
var resolveServeSettings = async (target, values, env = process.env) => {
|
|
4810
|
+
const flag = asString(values.settings);
|
|
4811
|
+
const source = flag ?? env.MOCKINGBIRD_SETTINGS;
|
|
4812
|
+
if (source === void 0 || source.trim() === "")
|
|
4813
|
+
return void 0;
|
|
4814
|
+
const label = flag !== void 0 ? "--settings" : "MOCKINGBIRD_SETTINGS";
|
|
4815
|
+
const parsed = await readJsonOption(source, label);
|
|
4816
|
+
if (!isObject2(parsed))
|
|
4817
|
+
throw new Error(`${label}: expected a JSON object`);
|
|
4818
|
+
return checkSettings(target, parsed, label);
|
|
4819
|
+
};
|
|
4541
4820
|
var resolveServeControls = async (values, env = process.env, base) => {
|
|
4542
4821
|
const source = asString(values["admin-controls"]) ?? env.MOCKINGBIRD_ADMIN_CONTROLS;
|
|
4543
4822
|
let document = isObject2(base) ? { ...base } : {};
|
|
4544
4823
|
if (source !== void 0 && source.trim() !== "") {
|
|
4545
|
-
|
|
4546
|
-
if (source.trim().startsWith("{")) {
|
|
4547
|
-
try {
|
|
4548
|
-
parsed = JSON.parse(source);
|
|
4549
|
-
} catch {
|
|
4550
|
-
throw new Error("--admin-controls: not valid JSON");
|
|
4551
|
-
}
|
|
4552
|
-
} else
|
|
4553
|
-
parsed = await readJsonFile(source, "--admin-controls");
|
|
4824
|
+
const parsed = source.trim().startsWith("{") ? await readJsonOption(source, "--admin-controls") : await readJsonFile(source, "--admin-controls");
|
|
4554
4825
|
if (!isObject2(parsed))
|
|
4555
4826
|
throw new Error("--admin-controls: expected a JSON object");
|
|
4556
4827
|
document = { ...document, ...parsed };
|
|
@@ -4592,26 +4863,7 @@ var resolveServeControls = async (values, env = process.env, base) => {
|
|
|
4592
4863
|
normalizeBootControls(document, "adminControls");
|
|
4593
4864
|
return document;
|
|
4594
4865
|
};
|
|
4595
|
-
var loadTarget =
|
|
4596
|
-
if (name === own.name)
|
|
4597
|
-
return own;
|
|
4598
|
-
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
4599
|
-
try {
|
|
4600
|
-
let mod;
|
|
4601
|
-
try {
|
|
4602
|
-
mod = await import(specifier);
|
|
4603
|
-
} catch {
|
|
4604
|
-
const fromProject = createRequire3(resolve3(process.cwd(), "package.json"));
|
|
4605
|
-
mod = await import(pathToFileURL(fromProject.resolve(specifier)).href);
|
|
4606
|
-
}
|
|
4607
|
-
if (!mod.serveTarget)
|
|
4608
|
-
throw new Error(`${specifier} exports no serveTarget`);
|
|
4609
|
-
return mod.serveTarget;
|
|
4610
|
-
} catch (error) {
|
|
4611
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4612
|
-
throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
|
|
4613
|
-
}
|
|
4614
|
-
};
|
|
4866
|
+
var loadTarget = (name, own) => createInstalledServiceLoader({ projectRoot: process.cwd(), services: [name], targets: [own] })(name);
|
|
4615
4867
|
var start = async (target, values, config) => {
|
|
4616
4868
|
const adminPrefix = resolveAdminPrefix(config.adminPrefix);
|
|
4617
4869
|
const runtime = await target.create(values, {
|
|
@@ -4619,8 +4871,11 @@ var start = async (target, values, config) => {
|
|
|
4619
4871
|
adminKey: config.adminKey,
|
|
4620
4872
|
seed: config.seed,
|
|
4621
4873
|
onLog: formatLog(config.log),
|
|
4874
|
+
...config.settings !== void 0 ? { settings: config.settings } : {},
|
|
4622
4875
|
...config.adminControls !== void 0 ? { adminControls: config.adminControls } : {}
|
|
4623
4876
|
});
|
|
4877
|
+
if (config.idBase !== void 0)
|
|
4878
|
+
runtime.setIdBase?.(config.idBase);
|
|
4624
4879
|
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
4625
4880
|
target.listening?.(listening);
|
|
4626
4881
|
console.log(`${target.name} emulator listening on ${listening.url}`);
|
|
@@ -4666,6 +4921,11 @@ var serveCommand = (target) => ({
|
|
|
4666
4921
|
const adminControls = await resolveServeControls(values, process.env, config.adminControls);
|
|
4667
4922
|
if (adminControls !== void 0)
|
|
4668
4923
|
config.adminControls = adminControls;
|
|
4924
|
+
if (values.settings !== void 0)
|
|
4925
|
+
throw new Error("--settings serves a single service; give each service its own `settings` in the config");
|
|
4926
|
+
const idBase = config.idBase ?? resolveServeIdBase(values);
|
|
4927
|
+
if (idBase !== void 0)
|
|
4928
|
+
config.idBase = idBase;
|
|
4669
4929
|
const fleet = await startFleet(config, {
|
|
4670
4930
|
load: (name) => loadTarget(name, target),
|
|
4671
4931
|
onLog: formatLog(config.log ?? log),
|
|
@@ -4695,8 +4955,12 @@ var serveCommand = (target) => ({
|
|
|
4695
4955
|
let listening;
|
|
4696
4956
|
try {
|
|
4697
4957
|
const adminControls = await resolveServeControls(values);
|
|
4958
|
+
const idBase = resolveServeIdBase(values);
|
|
4959
|
+
const settings = await resolveServeSettings(target, values);
|
|
4698
4960
|
listening = await start(target, values, {
|
|
4699
4961
|
...adminControls !== void 0 ? { adminControls } : {},
|
|
4962
|
+
...idBase !== void 0 ? { idBase } : {},
|
|
4963
|
+
...settings !== void 0 ? { settings } : {},
|
|
4700
4964
|
adminPrefix: resolveAdminPrefix(asString(values["admin-prefix"]) ?? process.env.MOCKINGBIRD_ADMIN_PREFIX),
|
|
4701
4965
|
port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
|
|
4702
4966
|
host: asString(values.host) ?? "127.0.0.1",
|