@open-mercato/cli 0.6.6-develop.5505.1.f08e81a6fe → 0.6.6-develop.5523.1.e223ca1915
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/single-instance-strategy-guard.js +89 -0
- package/dist/lib/single-instance-strategy-guard.js.map +7 -0
- package/dist/mercato.js +2 -0
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/__tests__/single-instance-strategy-guard.test.ts +138 -0
- package/src/lib/single-instance-strategy-guard.ts +172 -0
- package/src/mercato.ts +4 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
2
|
+
import { resolveQueueStrategy } from "@open-mercato/queue";
|
|
3
|
+
const MULTI_INSTANCE_SAFE_STRATEGIES = {
|
|
4
|
+
cache: ["redis"],
|
|
5
|
+
queue: ["async"],
|
|
6
|
+
rateLimit: ["redis"]
|
|
7
|
+
};
|
|
8
|
+
const COMPONENT_ENV_VARS = {
|
|
9
|
+
cache: "CACHE_STRATEGY",
|
|
10
|
+
queue: "QUEUE_STRATEGY",
|
|
11
|
+
rateLimit: "RATE_LIMIT_STRATEGY"
|
|
12
|
+
};
|
|
13
|
+
class SingleInstanceStrategyError extends Error {
|
|
14
|
+
constructor(result) {
|
|
15
|
+
super(
|
|
16
|
+
`Refusing to start: single-instance infrastructure strategies (${result.offenders.map((offender) => `${offender.envVar}=${offender.configured}`).join(", ")}) are configured under a declared multi-instance topology. Switch to a multi-instance-safe strategy or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override.`
|
|
17
|
+
);
|
|
18
|
+
this.name = "SingleInstanceStrategyError";
|
|
19
|
+
this.offenders = result.offenders;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function readInfraStrategySnapshot(env = process.env) {
|
|
23
|
+
return {
|
|
24
|
+
cacheStrategy: env.CACHE_STRATEGY?.trim() || "memory",
|
|
25
|
+
queueStrategy: resolveQueueStrategy(),
|
|
26
|
+
rateLimitStrategy: env.RATE_LIMIT_STRATEGY?.trim() || "memory"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveMultiInstanceHint(env) {
|
|
30
|
+
if (parseBooleanWithDefault(env.OM_MULTI_INSTANCE, false)) return true;
|
|
31
|
+
const instanceCount = Number.parseInt(env.OM_INSTANCE_COUNT ?? "", 10);
|
|
32
|
+
return Number.isFinite(instanceCount) && instanceCount > 1;
|
|
33
|
+
}
|
|
34
|
+
function evaluateSingleInstanceGuard(snapshot, env = process.env) {
|
|
35
|
+
const configured = {
|
|
36
|
+
cache: snapshot.cacheStrategy,
|
|
37
|
+
queue: snapshot.queueStrategy,
|
|
38
|
+
rateLimit: snapshot.rateLimitStrategy
|
|
39
|
+
};
|
|
40
|
+
const offenders = [];
|
|
41
|
+
for (const component of Object.keys(configured)) {
|
|
42
|
+
const safeValues = MULTI_INSTANCE_SAFE_STRATEGIES[component];
|
|
43
|
+
if (!safeValues.includes(configured[component])) {
|
|
44
|
+
offenders.push({
|
|
45
|
+
component,
|
|
46
|
+
envVar: COMPONENT_ENV_VARS[component],
|
|
47
|
+
configured: configured[component],
|
|
48
|
+
safeValues
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const production = (env.NODE_ENV ?? "").trim() === "production";
|
|
53
|
+
const multiInstance = resolveMultiInstanceHint(env);
|
|
54
|
+
const overridden = parseBooleanWithDefault(env.OM_ALLOW_SINGLE_INSTANCE_STRATEGIES, false);
|
|
55
|
+
let action = "ok";
|
|
56
|
+
if (offenders.length > 0 && production) {
|
|
57
|
+
action = multiInstance && !overridden ? "fail" : "warn";
|
|
58
|
+
}
|
|
59
|
+
return { action, offenders, production, multiInstance, overridden };
|
|
60
|
+
}
|
|
61
|
+
function formatSingleInstanceGuardMessage(result) {
|
|
62
|
+
const offenderLines = result.offenders.map(
|
|
63
|
+
(offender) => ` - ${offender.envVar}=${offender.configured} (multi-instance-safe: ${offender.safeValues.join(", ")})`
|
|
64
|
+
);
|
|
65
|
+
const header = result.action === "fail" ? "[server] Refusing to start: single-instance infrastructure strategies under a multi-instance topology." : "[server] WARNING: single-instance infrastructure strategies detected in production.";
|
|
66
|
+
const guidance = result.action === "fail" ? "[server] Switch each strategy above to a multi-instance-safe value, or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override (accepting duplicate jobs, stale ACLs, and weakened rate limits)." : result.multiInstance ? "[server] OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 is set \u2014 proceeding despite the risks above (duplicate jobs, stale ACLs, weakened rate limits)." : "[server] Running multiple instances against these strategies will cause duplicate jobs, stale ACLs, and weakened rate limits. Set OM_MULTI_INSTANCE=1 to make this a hard failure once you scale out.";
|
|
67
|
+
return [header, ...offenderLines, guidance];
|
|
68
|
+
}
|
|
69
|
+
function assertSingleInstanceStrategies(env = process.env, options) {
|
|
70
|
+
const snapshot = options?.snapshot ?? readInfraStrategySnapshot(env);
|
|
71
|
+
const result = evaluateSingleInstanceGuard(snapshot, env);
|
|
72
|
+
const logger = options?.logger ?? console;
|
|
73
|
+
if (result.action === "fail") {
|
|
74
|
+
for (const line of formatSingleInstanceGuardMessage(result)) logger.error(line);
|
|
75
|
+
throw new SingleInstanceStrategyError(result);
|
|
76
|
+
}
|
|
77
|
+
if (result.action === "warn") {
|
|
78
|
+
for (const line of formatSingleInstanceGuardMessage(result)) logger.warn(line);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
SingleInstanceStrategyError,
|
|
84
|
+
assertSingleInstanceStrategies,
|
|
85
|
+
evaluateSingleInstanceGuard,
|
|
86
|
+
formatSingleInstanceGuardMessage,
|
|
87
|
+
readInfraStrategySnapshot
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=single-instance-strategy-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/single-instance-strategy-guard.ts"],
|
|
4
|
+
"sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { resolveQueueStrategy } from '@open-mercato/queue'\n\n/**\n * Boot-time guard that refuses to start (or warns) when an infrastructure\n * strategy that is only safe for a single process/instance is configured under\n * a declared multi-instance topology.\n *\n * Background: `CACHE_STRATEGY`, `RATE_LIMIT_STRATEGY` and `QUEUE_STRATEGY` all\n * default to single-instance modes (`memory`/`memory`/`local`). Running more\n * than one app instance against those defaults silently breaks three\n * correctness invariants at once \u2014 stale RBAC caches after a privilege\n * revocation, rate limits multiplied by the instance count, and duplicate job\n * processing from the leaderless local file queue. None of them warn.\n *\n * Only strategies that coordinate across processes via shared external state\n * are considered multi-instance safe. Everything else (the in-process memory\n * strategies, the local file queue, and disk-backed caches that are not shared\n * across hosts) is treated as single-instance.\n */\nconst MULTI_INSTANCE_SAFE_STRATEGIES = {\n cache: ['redis'],\n queue: ['async'],\n rateLimit: ['redis'],\n} as const\n\nexport type SingleInstanceGuardComponent = 'cache' | 'queue' | 'rateLimit'\n\nexport type SingleInstanceGuardOffender = {\n component: SingleInstanceGuardComponent\n envVar: string\n configured: string\n safeValues: readonly string[]\n}\n\nexport type SingleInstanceGuardAction = 'ok' | 'warn' | 'fail'\n\nexport type SingleInstanceGuardResult = {\n action: SingleInstanceGuardAction\n offenders: SingleInstanceGuardOffender[]\n production: boolean\n multiInstance: boolean\n overridden: boolean\n}\n\nexport type InfraStrategySnapshot = {\n cacheStrategy: string\n queueStrategy: string\n rateLimitStrategy: string\n}\n\nconst COMPONENT_ENV_VARS: Record<SingleInstanceGuardComponent, string> = {\n cache: 'CACHE_STRATEGY',\n queue: 'QUEUE_STRATEGY',\n rateLimit: 'RATE_LIMIT_STRATEGY',\n}\n\nexport class SingleInstanceStrategyError extends Error {\n readonly offenders: SingleInstanceGuardOffender[]\n\n constructor(result: SingleInstanceGuardResult) {\n super(\n `Refusing to start: single-instance infrastructure strategies (${result.offenders\n .map((offender) => `${offender.envVar}=${offender.configured}`)\n .join(', ')}) are configured under a declared multi-instance topology. ` +\n 'Switch to a multi-instance-safe strategy or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override.',\n )\n this.name = 'SingleInstanceStrategyError'\n this.offenders = result.offenders\n }\n}\n\n/**\n * Read the configured infrastructure strategies. Queue resolution reuses the\n * canonical `resolveQueueStrategy()` so the default stays single-sourced; cache\n * and rate-limit values are read directly with their documented defaults\n * (`packages/cache/src/service.ts`, `packages/shared/src/lib/ratelimit/config.ts`)\n * because the guard's safe-set policy \u2014 not the resolver default \u2014 decides what\n * counts as single-instance.\n */\nexport function readInfraStrategySnapshot(env: NodeJS.ProcessEnv = process.env): InfraStrategySnapshot {\n return {\n cacheStrategy: env.CACHE_STRATEGY?.trim() || 'memory',\n queueStrategy: resolveQueueStrategy(),\n rateLimitStrategy: env.RATE_LIMIT_STRATEGY?.trim() || 'memory',\n }\n}\n\nfunction resolveMultiInstanceHint(env: NodeJS.ProcessEnv): boolean {\n if (parseBooleanWithDefault(env.OM_MULTI_INSTANCE, false)) return true\n const instanceCount = Number.parseInt(env.OM_INSTANCE_COUNT ?? '', 10)\n return Number.isFinite(instanceCount) && instanceCount > 1\n}\n\nexport function evaluateSingleInstanceGuard(\n snapshot: InfraStrategySnapshot,\n env: NodeJS.ProcessEnv = process.env,\n): SingleInstanceGuardResult {\n const configured: Record<SingleInstanceGuardComponent, string> = {\n cache: snapshot.cacheStrategy,\n queue: snapshot.queueStrategy,\n rateLimit: snapshot.rateLimitStrategy,\n }\n\n const offenders: SingleInstanceGuardOffender[] = []\n for (const component of Object.keys(configured) as SingleInstanceGuardComponent[]) {\n const safeValues: readonly string[] = MULTI_INSTANCE_SAFE_STRATEGIES[component]\n if (!safeValues.includes(configured[component])) {\n offenders.push({\n component,\n envVar: COMPONENT_ENV_VARS[component],\n configured: configured[component],\n safeValues,\n })\n }\n }\n\n const production = (env.NODE_ENV ?? '').trim() === 'production'\n const multiInstance = resolveMultiInstanceHint(env)\n const overridden = parseBooleanWithDefault(env.OM_ALLOW_SINGLE_INSTANCE_STRATEGIES, false)\n\n let action: SingleInstanceGuardAction = 'ok'\n if (offenders.length > 0 && production) {\n action = multiInstance && !overridden ? 'fail' : 'warn'\n }\n\n return { action, offenders, production, multiInstance, overridden }\n}\n\nexport function formatSingleInstanceGuardMessage(result: SingleInstanceGuardResult): string[] {\n const offenderLines = result.offenders.map(\n (offender) =>\n ` - ${offender.envVar}=${offender.configured} (multi-instance-safe: ${offender.safeValues.join(', ')})`,\n )\n const header =\n result.action === 'fail'\n ? '[server] Refusing to start: single-instance infrastructure strategies under a multi-instance topology.'\n : '[server] WARNING: single-instance infrastructure strategies detected in production.'\n const guidance =\n result.action === 'fail'\n ? '[server] Switch each strategy above to a multi-instance-safe value, or set OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 to override (accepting duplicate jobs, stale ACLs, and weakened rate limits).'\n : result.multiInstance\n ? '[server] OM_ALLOW_SINGLE_INSTANCE_STRATEGIES=1 is set \u2014 proceeding despite the risks above (duplicate jobs, stale ACLs, weakened rate limits).'\n : '[server] Running multiple instances against these strategies will cause duplicate jobs, stale ACLs, and weakened rate limits. Set OM_MULTI_INSTANCE=1 to make this a hard failure once you scale out.'\n return [header, ...offenderLines, guidance]\n}\n\nexport type SingleInstanceGuardLogger = Pick<Console, 'warn' | 'error'>\n\n/**\n * Evaluate the guard and enforce it: throw on `fail`, log prominently on\n * `warn`, and stay silent on `ok`. Safe to call on every `start`; it never\n * fires for dev boots or single-instance production deployments.\n */\nexport function assertSingleInstanceStrategies(\n env: NodeJS.ProcessEnv = process.env,\n options?: { snapshot?: InfraStrategySnapshot; logger?: SingleInstanceGuardLogger },\n): SingleInstanceGuardResult {\n const snapshot = options?.snapshot ?? readInfraStrategySnapshot(env)\n const result = evaluateSingleInstanceGuard(snapshot, env)\n const logger = options?.logger ?? console\n\n if (result.action === 'fail') {\n for (const line of formatSingleInstanceGuardMessage(result)) logger.error(line)\n throw new SingleInstanceStrategyError(result)\n }\n if (result.action === 'warn') {\n for (const line of formatSingleInstanceGuardMessage(result)) logger.warn(line)\n }\n\n return result\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,4BAA4B;AAmBrC,MAAM,iCAAiC;AAAA,EACrC,OAAO,CAAC,OAAO;AAAA,EACf,OAAO,CAAC,OAAO;AAAA,EACf,WAAW,CAAC,OAAO;AACrB;AA2BA,MAAM,qBAAmE;AAAA,EACvE,OAAO;AAAA,EACP,OAAO;AAAA,EACP,WAAW;AACb;AAEO,MAAM,oCAAoC,MAAM;AAAA,EAGrD,YAAY,QAAmC;AAC7C;AAAA,MACE,iEAAiE,OAAO,UACrE,IAAI,CAAC,aAAa,GAAG,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE,EAC7D,KAAK,IAAI,CAAC;AAAA,IAEf;AACA,SAAK,OAAO;AACZ,SAAK,YAAY,OAAO;AAAA,EAC1B;AACF;AAUO,SAAS,0BAA0B,MAAyB,QAAQ,KAA4B;AACrG,SAAO;AAAA,IACL,eAAe,IAAI,gBAAgB,KAAK,KAAK;AAAA,IAC7C,eAAe,qBAAqB;AAAA,IACpC,mBAAmB,IAAI,qBAAqB,KAAK,KAAK;AAAA,EACxD;AACF;AAEA,SAAS,yBAAyB,KAAiC;AACjE,MAAI,wBAAwB,IAAI,mBAAmB,KAAK,EAAG,QAAO;AAClE,QAAM,gBAAgB,OAAO,SAAS,IAAI,qBAAqB,IAAI,EAAE;AACrE,SAAO,OAAO,SAAS,aAAa,KAAK,gBAAgB;AAC3D;AAEO,SAAS,4BACd,UACA,MAAyB,QAAQ,KACN;AAC3B,QAAM,aAA2D;AAAA,IAC/D,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,WAAW,SAAS;AAAA,EACtB;AAEA,QAAM,YAA2C,CAAC;AAClD,aAAW,aAAa,OAAO,KAAK,UAAU,GAAqC;AACjF,UAAM,aAAgC,+BAA+B,SAAS;AAC9E,QAAI,CAAC,WAAW,SAAS,WAAW,SAAS,CAAC,GAAG;AAC/C,gBAAU,KAAK;AAAA,QACb;AAAA,QACA,QAAQ,mBAAmB,SAAS;AAAA,QACpC,YAAY,WAAW,SAAS;AAAA,QAChC;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,cAAc,IAAI,YAAY,IAAI,KAAK,MAAM;AACnD,QAAM,gBAAgB,yBAAyB,GAAG;AAClD,QAAM,aAAa,wBAAwB,IAAI,qCAAqC,KAAK;AAEzF,MAAI,SAAoC;AACxC,MAAI,UAAU,SAAS,KAAK,YAAY;AACtC,aAAS,iBAAiB,CAAC,aAAa,SAAS;AAAA,EACnD;AAEA,SAAO,EAAE,QAAQ,WAAW,YAAY,eAAe,WAAW;AACpE;AAEO,SAAS,iCAAiC,QAA6C;AAC5F,QAAM,gBAAgB,OAAO,UAAU;AAAA,IACrC,CAAC,aACC,OAAO,SAAS,MAAM,IAAI,SAAS,UAAU,0BAA0B,SAAS,WAAW,KAAK,IAAI,CAAC;AAAA,EACzG;AACA,QAAM,SACJ,OAAO,WAAW,SACd,2GACA;AACN,QAAM,WACJ,OAAO,WAAW,SACd,mMACA,OAAO,gBACL,wJACA;AACR,SAAO,CAAC,QAAQ,GAAG,eAAe,QAAQ;AAC5C;AASO,SAAS,+BACd,MAAyB,QAAQ,KACjC,SAC2B;AAC3B,QAAM,WAAW,SAAS,YAAY,0BAA0B,GAAG;AACnE,QAAM,SAAS,4BAA4B,UAAU,GAAG;AACxD,QAAM,SAAS,SAAS,UAAU;AAElC,MAAI,OAAO,WAAW,QAAQ;AAC5B,eAAW,QAAQ,iCAAiC,MAAM,EAAG,QAAO,MAAM,IAAI;AAC9E,UAAM,IAAI,4BAA4B,MAAM;AAAA,EAC9C;AACA,MAAI,OAAO,WAAW,QAAQ;AAC5B,eAAW,QAAQ,iCAAiC,MAAM,EAAG,QAAO,KAAK,IAAI;AAAA,EAC/E;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/mercato.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { parseModuleInstallArgs } from "./lib/module-install-args.js";
|
|
26
26
|
import { resolveNextBuildIdCandidate } from "./lib/next-build-id.js";
|
|
27
27
|
import { acquireServerStartLock } from "./lib/server-start-lock.js";
|
|
28
|
+
import { assertSingleInstanceStrategies } from "./lib/single-instance-strategy-guard.js";
|
|
28
29
|
import { createDevEnvReloader, watchDevEnvFiles } from "./lib/dev-env-reload.js";
|
|
29
30
|
const lazyIntegration = () => import("./lib/testing/integration.js");
|
|
30
31
|
import path from "node:path";
|
|
@@ -1781,6 +1782,7 @@ async function run(argv = process.argv) {
|
|
|
1781
1782
|
const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env);
|
|
1782
1783
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
1783
1784
|
const runtimeEnv = buildServerProcessEnvironment(process.env);
|
|
1785
|
+
assertSingleInstanceStrategies(runtimeEnv);
|
|
1784
1786
|
const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
|
|
1785
1787
|
const serverStartLock = acquireServerStartLock(appDir, {
|
|
1786
1788
|
port: runtimeEnv.PORT ?? process.env.PORT ?? null
|