@open-mercato/cli 0.6.6-develop.5509.1.006f4d4f24 → 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.
@@ -1,4 +1,4 @@
1
- [build:cli] found 87 entry points
1
+ [build:cli] found 88 entry points
2
2
  Copied create-app/agentic/ → dist/agentic/
3
3
  Discovered 16 standalone guides → dist/agentic/guides/
4
4
  [build:cli] built successfully
@@ -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