@open-mercato/cli 0.8.1-develop.7296.1.2111d779db → 0.9.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.
Files changed (217) hide show
  1. package/.turbo/turbo-build.log +8 -2
  2. package/AGENTS.md +2 -0
  3. package/dist/agentic/guides/module-facts.json +7612 -848
  4. package/dist/agentic/guides/module-facts.v2.json +7612 -848
  5. package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +1 -1
  6. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  7. package/dist/agentic/guides/modules/ai_assistant/encryption.md +13 -0
  8. package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -0
  9. package/dist/agentic/guides/modules/ai_assistant/index.md +4 -3
  10. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  11. package/dist/agentic/guides/modules/api_keys/domain-commands.md +12 -0
  12. package/dist/agentic/guides/modules/api_keys/index.md +3 -2
  13. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  14. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  15. package/dist/agentic/guides/modules/auth/index.md +1 -1
  16. package/dist/agentic/guides/modules/availability/acl-features.md +13 -0
  17. package/dist/agentic/guides/modules/availability/backend-pages.md +14 -0
  18. package/dist/agentic/guides/modules/availability/di-registrations-rich.md +11 -0
  19. package/dist/agentic/guides/modules/availability/domain-commands.md +13 -0
  20. package/dist/agentic/guides/modules/availability/entities.md +11 -0
  21. package/dist/agentic/guides/modules/availability/events.md +13 -0
  22. package/dist/agentic/guides/modules/availability/exact-override-targets.md +19 -0
  23. package/dist/agentic/guides/modules/availability/host-extension-points.md +10 -0
  24. package/dist/agentic/guides/modules/availability/index.md +22 -0
  25. package/dist/agentic/guides/modules/availability/owned-contract-module-metadata.md +11 -0
  26. package/dist/agentic/guides/modules/availability/setup.md +11 -0
  27. package/dist/agentic/guides/modules/availability/umes-hosts.md +14 -0
  28. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  29. package/dist/agentic/guides/modules/catalog/active-extension-bindings.md +1 -1
  30. package/dist/agentic/guides/modules/catalog/di-registrations-rich.md +1 -0
  31. package/dist/agentic/guides/modules/catalog/exact-override-targets.md +3 -0
  32. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  33. package/dist/agentic/guides/modules/catalog/workers.md +2 -0
  34. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  35. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  36. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  37. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  38. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  39. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  40. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  41. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  42. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  43. package/dist/agentic/guides/modules/communication_channels/acl-features.md +2 -1
  44. package/dist/agentic/guides/modules/communication_channels/contribution-resolutions.md +1 -0
  45. package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +11 -10
  46. package/dist/agentic/guides/modules/communication_channels/domain-commands.md +1 -0
  47. package/dist/agentic/guides/modules/communication_channels/events.md +2 -1
  48. package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +12 -10
  49. package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +1 -0
  50. package/dist/agentic/guides/modules/communication_channels/index.md +2 -2
  51. package/dist/agentic/guides/modules/communication_channels/umes-contributions.md +1 -0
  52. package/dist/agentic/guides/modules/communication_channels/umes-hosts.md +1 -0
  53. package/dist/agentic/guides/modules/configs/index.md +1 -1
  54. package/dist/agentic/guides/modules/content/index.md +1 -1
  55. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  56. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  57. package/dist/agentic/guides/modules/customer_groups/acl-features.md +16 -0
  58. package/dist/agentic/guides/modules/customer_groups/active-extension-bindings.md +13 -0
  59. package/dist/agentic/guides/modules/customer_groups/backend-pages.md +14 -0
  60. package/dist/agentic/guides/modules/customer_groups/cli-commands.md +11 -0
  61. package/dist/agentic/guides/modules/customer_groups/contribution-resolutions.md +13 -0
  62. package/dist/agentic/guides/modules/customer_groups/di-registrations-rich.md +13 -0
  63. package/dist/agentic/guides/modules/customer_groups/di-service-tokens.md +11 -0
  64. package/dist/agentic/guides/modules/customer_groups/domain-commands.md +11 -0
  65. package/dist/agentic/guides/modules/customer_groups/entities.md +13 -0
  66. package/dist/agentic/guides/modules/customer_groups/events.md +17 -0
  67. package/dist/agentic/guides/modules/customer_groups/exact-override-targets.md +27 -0
  68. package/dist/agentic/guides/modules/customer_groups/index.md +26 -0
  69. package/dist/agentic/guides/modules/customer_groups/owned-contract-module-metadata.md +11 -0
  70. package/dist/agentic/guides/modules/customer_groups/setup.md +11 -0
  71. package/dist/agentic/guides/modules/customer_groups/umes-contributions.md +13 -0
  72. package/dist/agentic/guides/modules/customer_groups/umes-hosts.md +21 -0
  73. package/dist/agentic/guides/modules/customers/acl-features.md +2 -1
  74. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +8 -7
  75. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
  76. package/dist/agentic/guides/modules/customers/domain-commands.md +1 -0
  77. package/dist/agentic/guides/modules/customers/entities.md +1 -0
  78. package/dist/agentic/guides/modules/customers/events.md +2 -1
  79. package/dist/agentic/guides/modules/customers/exact-override-targets.md +2 -0
  80. package/dist/agentic/guides/modules/customers/index.md +3 -3
  81. package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
  82. package/dist/agentic/guides/modules/customers/umes-hosts.md +3 -0
  83. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  84. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  85. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  86. package/dist/agentic/guides/modules/devices/index.md +1 -1
  87. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  88. package/dist/agentic/guides/modules/directory/active-extension-bindings.md +1 -1
  89. package/dist/agentic/guides/modules/directory/index.md +1 -1
  90. package/dist/agentic/guides/modules/documents/index.md +1 -1
  91. package/dist/agentic/guides/modules/entities/active-extension-bindings.md +1 -1
  92. package/dist/agentic/guides/modules/entities/index.md +1 -1
  93. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  94. package/dist/agentic/guides/modules/events/index.md +1 -1
  95. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  96. package/dist/agentic/guides/modules/forms/acl-features.md +16 -0
  97. package/dist/agentic/guides/modules/forms/active-extension-bindings.md +14 -0
  98. package/dist/agentic/guides/modules/forms/backend-pages.md +17 -0
  99. package/dist/agentic/guides/modules/forms/contribution-resolutions.md +21 -0
  100. package/dist/agentic/guides/modules/forms/di-registrations-rich.md +27 -0
  101. package/dist/agentic/guides/modules/forms/di-service-tokens.md +19 -0
  102. package/dist/agentic/guides/modules/forms/domain-commands.md +30 -0
  103. package/dist/agentic/guides/modules/forms/encryption.md +11 -0
  104. package/dist/agentic/guides/modules/forms/entities.md +21 -0
  105. package/dist/agentic/guides/modules/forms/events.md +28 -0
  106. package/dist/agentic/guides/modules/forms/exact-override-targets.md +59 -0
  107. package/dist/agentic/guides/modules/forms/frontend-pages.md +16 -0
  108. package/dist/agentic/guides/modules/forms/index.md +28 -0
  109. package/dist/agentic/guides/modules/forms/owned-contract-module-metadata.md +11 -0
  110. package/dist/agentic/guides/modules/forms/setup.md +11 -0
  111. package/dist/agentic/guides/modules/forms/umes-contributions.md +21 -0
  112. package/dist/agentic/guides/modules/forms/umes-hosts.md +43 -0
  113. package/dist/agentic/guides/modules/forms/workers.md +12 -0
  114. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  115. package/dist/agentic/guides/modules/generators/index.md +1 -1
  116. package/dist/agentic/guides/modules/inbox_ops/active-extension-bindings.md +1 -1
  117. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  118. package/dist/agentic/guides/modules/integrations/di-registrations-rich.md +8 -8
  119. package/dist/agentic/guides/modules/integrations/di-service-tokens.md +4 -4
  120. package/dist/agentic/guides/modules/integrations/exact-override-targets.md +8 -8
  121. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  122. package/dist/agentic/guides/modules/messages/index.md +1 -1
  123. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  124. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  125. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  126. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  127. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  128. package/dist/agentic/guides/modules/planner/active-extension-bindings.md +1 -1
  129. package/dist/agentic/guides/modules/planner/index.md +1 -1
  130. package/dist/agentic/guides/modules/portal/index.md +1 -1
  131. package/dist/agentic/guides/modules/progress/index.md +1 -1
  132. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  133. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  134. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  135. package/dist/agentic/guides/modules/resources/active-extension-bindings.md +1 -1
  136. package/dist/agentic/guides/modules/resources/index.md +1 -1
  137. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +1 -1
  138. package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -4
  139. package/dist/agentic/guides/modules/sales/index.md +1 -1
  140. package/dist/agentic/guides/modules/sales/setup.md +1 -1
  141. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  142. package/dist/agentic/guides/modules/search/index.md +1 -1
  143. package/dist/agentic/guides/modules/security/index.md +1 -1
  144. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  145. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  146. package/dist/agentic/guides/modules/sso/index.md +1 -1
  147. package/dist/agentic/guides/modules/staff/active-extension-bindings.md +1 -1
  148. package/dist/agentic/guides/modules/staff/index.md +1 -1
  149. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  150. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  151. package/dist/agentic/guides/modules/sync_excel/active-extension-bindings.md +11 -0
  152. package/dist/agentic/guides/modules/sync_excel/index.md +3 -2
  153. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  154. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  155. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  156. package/dist/agentic/guides/modules/translations/index.md +1 -1
  157. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  158. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  159. package/dist/agentic/guides/modules/wms/active-extension-bindings.md +1 -1
  160. package/dist/agentic/guides/modules/wms/contribution-resolutions.md +3 -0
  161. package/dist/agentic/guides/modules/wms/di-registrations-rich.md +8 -8
  162. package/dist/agentic/guides/modules/wms/exact-override-targets.md +11 -8
  163. package/dist/agentic/guides/modules/wms/index.md +3 -3
  164. package/dist/agentic/guides/modules/wms/umes-contributions.md +3 -0
  165. package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +3 -3
  166. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  167. package/dist/agentic/guides/reference-module-facts.json +1 -1
  168. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +73 -1
  169. package/dist/agentic/guides/upstream/manifest.json +2 -2
  170. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.8.0-to-0.9.0/SKILL.md +112 -0
  171. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +1 -1
  172. package/dist/agentic/shared/ai/skills/tiers.json +2 -1
  173. package/dist/bin.js +2 -2
  174. package/dist/bin.js.map +2 -2
  175. package/dist/lib/db/commands.js +84 -5
  176. package/dist/lib/db/commands.js.map +2 -2
  177. package/dist/lib/db/encryption-backfill-check.js +112 -0
  178. package/dist/lib/db/encryption-backfill-check.js.map +7 -0
  179. package/dist/lib/db/migration-reindex.js +2 -1
  180. package/dist/lib/db/migration-reindex.js.map +2 -2
  181. package/dist/lib/events-single-delivery.js.map +2 -2
  182. package/dist/lib/flush-before-exit.js +19 -0
  183. package/dist/lib/flush-before-exit.js.map +7 -0
  184. package/dist/lib/generate-watch-structure.js +113 -19
  185. package/dist/lib/generate-watch-structure.js.map +3 -3
  186. package/dist/lib/generators/module-registry.js +4 -2
  187. package/dist/lib/generators/module-registry.js.map +2 -2
  188. package/dist/lib/generators/web-research-adapters.js +32 -11
  189. package/dist/lib/generators/web-research-adapters.js.map +2 -2
  190. package/dist/lib/telemetry-init.js +69 -11
  191. package/dist/lib/telemetry-init.js.map +3 -3
  192. package/dist/lib/testing/integration.js +29 -2
  193. package/dist/lib/testing/integration.js.map +2 -2
  194. package/package.json +8 -8
  195. package/src/bin.ts +4 -4
  196. package/src/lib/__tests__/flush-before-exit.test.ts +32 -0
  197. package/src/lib/__tests__/generate-watch-structure.test.ts +261 -3
  198. package/src/lib/__tests__/telemetry-init.test.ts +89 -0
  199. package/src/lib/db/__tests__/commands.test.ts +43 -0
  200. package/src/lib/db/__tests__/dynamic-import-specifier.test.ts +36 -0
  201. package/src/lib/db/__tests__/encryption-backfill-check.test.ts +146 -0
  202. package/src/lib/db/__tests__/migration-reindex.test.ts +48 -3
  203. package/src/lib/db/commands.ts +113 -10
  204. package/src/lib/db/encryption-backfill-check.ts +163 -0
  205. package/src/lib/db/migration-reindex.ts +2 -1
  206. package/src/lib/events-single-delivery.ts +5 -2
  207. package/src/lib/flush-before-exit.ts +39 -0
  208. package/src/lib/generate-watch-structure.ts +134 -19
  209. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +14 -3
  210. package/src/lib/generators/__tests__/module-package-sources.test.ts +2 -2
  211. package/src/lib/generators/__tests__/registry-variant-parity.test.ts +16 -0
  212. package/src/lib/generators/__tests__/web-research-adapters.test.ts +141 -0
  213. package/src/lib/generators/module-registry.ts +2 -0
  214. package/src/lib/generators/web-research-adapters.ts +41 -20
  215. package/src/lib/telemetry-init.ts +68 -11
  216. package/src/lib/testing/__tests__/integration.test.ts +26 -0
  217. package/src/lib/testing/integration.ts +32 -2
@@ -44,8 +44,36 @@ function scanDirectory(root, results) {
44
44
  if (discovered && !results.has(discovered.packageName)) results.set(discovered.packageName, discovered);
45
45
  }
46
46
  }
47
- function identifierFor(index) {
48
- return `adapter${index}`;
47
+ function renderEntries(adapters) {
48
+ if (adapters.length === 0) return "export const webResearchAdapterEntries: AdapterRegistryEntry[] = []\n";
49
+ const loaders = adapters.map(
50
+ (adapter) => ` loadAdapterEntry('${adapter.packageName}', () => import('${adapter.packageName}')),`
51
+ ).join("\n");
52
+ return `const MISSING_MODULE_CODES = new Set(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'])
53
+
54
+ async function loadAdapterEntry(
55
+ packageName: string,
56
+ load: () => Promise<unknown>,
57
+ ): Promise<AdapterRegistryEntry | null> {
58
+ try {
59
+ return { packageName, module: await load() }
60
+ } catch (error) {
61
+ const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown }
62
+ const isMissingCode = typeof code === 'string' && MISSING_MODULE_CODES.has(code)
63
+ const namesThisPackage = typeof message === 'string' && message.includes(\`'\${packageName}'\`)
64
+ if (isMissingCode && namesThisPackage) return null
65
+ throw error
66
+ }
67
+ }
68
+
69
+ const loadedEntries = await Promise.all([
70
+ ${loaders}
71
+ ])
72
+
73
+ export const webResearchAdapterEntries: AdapterRegistryEntry[] = loadedEntries.filter(
74
+ (entry): entry is AdapterRegistryEntry => entry !== null,
75
+ )
76
+ `;
49
77
  }
50
78
  async function generateWebResearchAdapters(options) {
51
79
  const { resolver, quiet } = options;
@@ -65,20 +93,13 @@ async function generateWebResearchAdapters(options) {
65
93
  const adapters = [...discovered.values()].sort(
66
94
  (left, right) => left.packageName.localeCompare(right.packageName)
67
95
  );
68
- const imports = adapters.map((adapter, index) => `import * as ${identifierFor(index)} from '${adapter.packageName}'`).join("\n");
69
- const entries = adapters.map(
70
- (adapter, index) => ` { packageName: '${adapter.packageName}', module: ${identifierFor(index)} },`
71
- ).join("\n");
72
96
  const content = `// AUTO-GENERATED \u2014 do not edit by hand.
73
97
  // Source: packages declaring \`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\` in package.json.
74
98
  // Regenerate with: yarn generate
99
+ // Adapter packages that are not installed where this registry runs are skipped.
75
100
  import type { AdapterRegistryEntry } from '@open-mercato/web-research'
76
- ${imports}
77
101
 
78
- export const webResearchAdapterEntries: AdapterRegistryEntry[] = [
79
- ${entries}
80
- ]
81
- `;
102
+ ${renderEntries(adapters)}`;
82
103
  writeGeneratedFile({
83
104
  outFile,
84
105
  checksumFile,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/generators/web-research-adapters.ts"],
4
- "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\nimport type { PackageResolver } from '../resolver'\nimport { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'\n\nexport interface WebResearchAdaptersOptions {\n resolver: PackageResolver\n quiet?: boolean\n}\n\n/**\n * Mirrors `openMercato.webResearchAdapter` in `@open-mercato/web-research`. It is\n * duplicated rather than imported because the CLI must not depend on the engine\n * package to run its generators.\n */\nconst MANIFEST_NAMESPACE = 'openMercato'\nconst ADAPTER_MANIFEST_KEY = 'webResearchAdapter'\n\ntype DiscoveredAdapter = {\n packageName: string\n adapterId: string\n sourceRoot: string\n}\n\nfunction readJson(file: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8'))\n } catch {\n return null\n }\n}\n\nfunction readManifest(packageJsonPath: string): DiscoveredAdapter | null {\n const parsed = readJson(packageJsonPath)\n if (typeof parsed !== 'object' || parsed === null) return null\n const pkg = parsed as Record<string, unknown>\n const namespace = pkg[MANIFEST_NAMESPACE]\n if (typeof namespace !== 'object' || namespace === null) return null\n const manifest = (namespace as Record<string, unknown>)[ADAPTER_MANIFEST_KEY]\n if (typeof manifest !== 'object' || manifest === null) return null\n const adapterId = (manifest as Record<string, unknown>).id\n const packageName = pkg.name\n if (typeof packageName !== 'string' || packageName.length === 0) return null\n if (typeof adapterId !== 'string' || adapterId.length === 0) return null\n return { packageName, adapterId, sourceRoot: path.dirname(packageJsonPath) }\n}\n\nfunction scanDirectory(root: string, results: Map<string, DiscoveredAdapter>): void {\n if (!fs.existsSync(root)) return\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (!entry.isDirectory() && !entry.isSymbolicLink()) continue\n if (entry.name === '.bin' || entry.name === '.cache') continue\n const child = path.join(root, entry.name)\n // Scoped packages nest one level deeper (`@scope/name`).\n if (entry.name.startsWith('@')) {\n scanDirectory(child, results)\n continue\n }\n const discovered = readManifest(path.join(child, 'package.json'))\n if (discovered && !results.has(discovered.packageName)) results.set(discovered.packageName, discovered)\n }\n}\n\nfunction identifierFor(index: number): string {\n return `adapter${index}`\n}\n\n/**\n * Emits the statically imported adapter registry. Discovery happens here, at\n * build time, because a bundler cannot follow `import()` with a runtime-computed\n * specifier \u2014 a dynamic registry would resolve to nothing under Turbopack.\n */\nexport async function generateWebResearchAdapters(\n options: WebResearchAdaptersOptions,\n): Promise<GeneratorResult> {\n const { resolver, quiet } = options\n const result = createGeneratorResult()\n const outputDir = resolver.getOutputDir()\n const outFile = path.join(outputDir, 'web-research-adapters.generated.ts')\n const checksumFile = path.join(outputDir, 'web-research-adapters.checksum')\n\n const appDir = resolver.getAppDir()\n const repoRoot = path.resolve(appDir, '..', '..')\n const discovered = new Map<string, DiscoveredAdapter>()\n const scanRoots = [\n path.join(repoRoot, 'packages'),\n path.join(repoRoot, 'node_modules'),\n path.join(appDir, 'node_modules'),\n ]\n for (const root of scanRoots) scanDirectory(root, discovered)\n\n const adapters = [...discovered.values()].sort((left, right) =>\n left.packageName.localeCompare(right.packageName),\n )\n\n const imports = adapters\n .map((adapter, index) => `import * as ${identifierFor(index)} from '${adapter.packageName}'`)\n .join('\\n')\n const entries = adapters\n .map(\n (adapter, index) =>\n ` { packageName: '${adapter.packageName}', module: ${identifierFor(index)} },`,\n )\n .join('\\n')\n\n const content = `// AUTO-GENERATED \u2014 do not edit by hand.\n// Source: packages declaring \\`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\\` in package.json.\n// Regenerate with: yarn generate\nimport type { AdapterRegistryEntry } from '@open-mercato/web-research'\n${imports}\n\nexport const webResearchAdapterEntries: AdapterRegistryEntry[] = [\n${entries}\n]\n`\n\n writeGeneratedFile({\n outFile,\n checksumFile,\n content,\n structureChecksum: calculateStructureChecksum(adapters.map((adapter) => adapter.sourceRoot)),\n result,\n quiet,\n })\n\n return result\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\nimport type { PackageResolver } from '../resolver'\nimport { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'\n\nexport interface WebResearchAdaptersOptions {\n resolver: PackageResolver\n quiet?: boolean\n}\n\n/**\n * Mirrors `openMercato.webResearchAdapter` in `@open-mercato/web-research`. It is\n * duplicated rather than imported because the CLI must not depend on the engine\n * package to run its generators.\n */\nconst MANIFEST_NAMESPACE = 'openMercato'\nconst ADAPTER_MANIFEST_KEY = 'webResearchAdapter'\n\ntype DiscoveredAdapter = {\n packageName: string\n adapterId: string\n sourceRoot: string\n}\n\nfunction readJson(file: string): unknown {\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8'))\n } catch {\n return null\n }\n}\n\nfunction readManifest(packageJsonPath: string): DiscoveredAdapter | null {\n const parsed = readJson(packageJsonPath)\n if (typeof parsed !== 'object' || parsed === null) return null\n const pkg = parsed as Record<string, unknown>\n const namespace = pkg[MANIFEST_NAMESPACE]\n if (typeof namespace !== 'object' || namespace === null) return null\n const manifest = (namespace as Record<string, unknown>)[ADAPTER_MANIFEST_KEY]\n if (typeof manifest !== 'object' || manifest === null) return null\n const adapterId = (manifest as Record<string, unknown>).id\n const packageName = pkg.name\n if (typeof packageName !== 'string' || packageName.length === 0) return null\n if (typeof adapterId !== 'string' || adapterId.length === 0) return null\n return { packageName, adapterId, sourceRoot: path.dirname(packageJsonPath) }\n}\n\nfunction scanDirectory(root: string, results: Map<string, DiscoveredAdapter>): void {\n if (!fs.existsSync(root)) return\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (!entry.isDirectory() && !entry.isSymbolicLink()) continue\n if (entry.name === '.bin' || entry.name === '.cache') continue\n const child = path.join(root, entry.name)\n // Scoped packages nest one level deeper (`@scope/name`).\n if (entry.name.startsWith('@')) {\n scanDirectory(child, results)\n continue\n }\n const discovered = readManifest(path.join(child, 'package.json'))\n if (discovered && !results.has(discovered.packageName)) results.set(discovered.packageName, discovered)\n }\n}\n\nfunction renderEntries(adapters: readonly DiscoveredAdapter[]): string {\n if (adapters.length === 0) return 'export const webResearchAdapterEntries: AdapterRegistryEntry[] = []\\n'\n const loaders = adapters\n .map(\n (adapter) =>\n ` loadAdapterEntry('${adapter.packageName}', () => import('${adapter.packageName}')),`,\n )\n .join('\\n')\n return `const MISSING_MODULE_CODES = new Set(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'])\n\nasync function loadAdapterEntry(\n packageName: string,\n load: () => Promise<unknown>,\n): Promise<AdapterRegistryEntry | null> {\n try {\n return { packageName, module: await load() }\n } catch (error) {\n const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown }\n const isMissingCode = typeof code === 'string' && MISSING_MODULE_CODES.has(code)\n const namesThisPackage = typeof message === 'string' && message.includes(\\`'\\${packageName}'\\`)\n if (isMissingCode && namesThisPackage) return null\n throw error\n }\n}\n\nconst loadedEntries = await Promise.all([\n${loaders}\n])\n\nexport const webResearchAdapterEntries: AdapterRegistryEntry[] = loadedEntries.filter(\n (entry): entry is AdapterRegistryEntry => entry !== null,\n)\n`\n}\n\n/**\n * Emits the adapter registry. Discovery happens here, at build time, because a\n * bundler cannot follow `import()` with a runtime-computed specifier \u2014 a dynamic\n * registry would resolve to nothing under Turbopack. Each adapter is imported\n * with a literal specifier and a package that is not installed where the\n * registry runs is skipped: discovery scans the whole workspace, but a process\n * such as a worker in a production image only has the app's own dependencies.\n */\nexport async function generateWebResearchAdapters(\n options: WebResearchAdaptersOptions,\n): Promise<GeneratorResult> {\n const { resolver, quiet } = options\n const result = createGeneratorResult()\n const outputDir = resolver.getOutputDir()\n const outFile = path.join(outputDir, 'web-research-adapters.generated.ts')\n const checksumFile = path.join(outputDir, 'web-research-adapters.checksum')\n\n const appDir = resolver.getAppDir()\n const repoRoot = path.resolve(appDir, '..', '..')\n const discovered = new Map<string, DiscoveredAdapter>()\n const scanRoots = [\n path.join(repoRoot, 'packages'),\n path.join(repoRoot, 'node_modules'),\n path.join(appDir, 'node_modules'),\n ]\n for (const root of scanRoots) scanDirectory(root, discovered)\n\n const adapters = [...discovered.values()].sort((left, right) =>\n left.packageName.localeCompare(right.packageName),\n )\n\n const content = `// AUTO-GENERATED \u2014 do not edit by hand.\n// Source: packages declaring \\`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\\` in package.json.\n// Regenerate with: yarn generate\n// Adapter packages that are not installed where this registry runs are skipped.\nimport type { AdapterRegistryEntry } from '@open-mercato/web-research'\n\n${renderEntries(adapters)}`\n\n writeGeneratedFile({\n outFile,\n checksumFile,\n content,\n structureChecksum: calculateStructureChecksum(adapters.map((adapter) => adapter.sourceRoot)),\n result,\n quiet,\n })\n\n return result\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -56,16 +56,38 @@ export async function register(): Promise<void> {
56
56
  // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK
57
57
  // is Node-only and incompatible with the edge runtime, so the telemetry
58
58
  // bootstrap \u2014 which can pull in the SDK \u2014 is imported only on the Node.js
59
- // runtime. The helper owns init + graceful degrade + shutdown flush.
59
+ // runtime. The helper owns init + graceful degrade + shutdown flush; a
60
+ // deliberate configuration failure bubbles here so the host exits loudly.
60
61
  if (
61
62
  process.env.NEXT_RUNTIME === 'nodejs'
62
63
  && isTelemetryBackendEnabled()
63
64
  ) {
64
65
  const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')
65
- await registerTelemetryForNextjs()
66
+ try {
67
+ await registerTelemetryForNextjs()
68
+ } catch (err) {
69
+ const nodeProcess = process
70
+ nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)
71
+ nodeProcess.exit(1)
72
+ }
66
73
  }
67
74
  }
68
75
  `;
76
+ const MULTILINE_TELEMETRY_GUARD = ` if (
77
+ process.env.NEXT_RUNTIME === 'nodejs'
78
+ && isTelemetryBackendEnabled()
79
+ ) {`;
80
+ const INLINE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`;
81
+ const LEGACY_NODE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs') {`;
82
+ const TELEMETRY_DYNAMIC_IMPORT = ` const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')`;
83
+ const BARE_TELEMETRY_REGISTRATION = ` await registerTelemetryForNextjs()`;
84
+ const GUARDED_TELEMETRY_REGISTRATION = ` try {
85
+ await registerTelemetryForNextjs()
86
+ } catch (err) {
87
+ const nodeProcess = process
88
+ nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)
89
+ nodeProcess.exit(1)
90
+ }`;
69
91
  const DISPATCHER_SNIPPET = ` // add near the other imports:
70
92
  import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
71
93
 
@@ -233,16 +255,46 @@ function patchInstrumentation(appDir, options) {
233
255
  }
234
256
  const content = readFileSync(path, "utf8");
235
257
  if (content.includes("registerTelemetryForNextjs")) {
236
- if (content.includes("isTelemetryBackendEnabled")) {
258
+ const guardedBootstraps = [MULTILINE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD];
259
+ const safeBootstrap = guardedBootstraps.map((guard) => `${guard}
260
+ ${TELEMETRY_DYNAMIC_IMPORT}
261
+ ${GUARDED_TELEMETRY_REGISTRATION}`).find((bootstrap) => content.includes(bootstrap));
262
+ if (safeBootstrap) {
237
263
  return { file, status: "skipped", detail: "telemetry already wired" };
238
264
  }
239
- const migrated = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'
240
- ` + content.replace(
241
- `if (process.env.NEXT_RUNTIME === 'nodejs') {`,
242
- `if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`
243
- );
244
- if (!options.dryRun) writeFileSync(path, migrated);
245
- return { file, status: "patched", detail: "added the explicit backend import guard" };
265
+ const guardedLegacyBootstrap = guardedBootstraps.map((guard) => `${guard}
266
+ ${TELEMETRY_DYNAMIC_IMPORT}
267
+ ${BARE_TELEMETRY_REGISTRATION}`).find((bootstrap) => content.includes(bootstrap));
268
+ if (guardedLegacyBootstrap) {
269
+ const migrated = content.replace(
270
+ guardedLegacyBootstrap,
271
+ guardedLegacyBootstrap.replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION)
272
+ );
273
+ if (!options.dryRun) writeFileSync(path, migrated);
274
+ return { file, status: "patched", detail: "wrapped OTLP dependency failures with host termination" };
275
+ }
276
+ const nodeOnlyLegacyBootstrap = `${LEGACY_NODE_TELEMETRY_GUARD}
277
+ ${TELEMETRY_DYNAMIC_IMPORT}
278
+ ${BARE_TELEMETRY_REGISTRATION}`;
279
+ if (content.includes(nodeOnlyLegacyBootstrap)) {
280
+ const guardedBootstrap = nodeOnlyLegacyBootstrap.replace(LEGACY_NODE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD).replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION);
281
+ const importLine2 = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'
282
+ `;
283
+ const withBootstrap = content.replace(nodeOnlyLegacyBootstrap, guardedBootstrap);
284
+ const migrated = withBootstrap.includes(importLine2) ? withBootstrap : importLine2 + withBootstrap;
285
+ if (!options.dryRun) writeFileSync(path, migrated);
286
+ return {
287
+ file,
288
+ status: "patched",
289
+ detail: "added the explicit backend guard and wrapped OTLP dependency failures with host termination"
290
+ };
291
+ }
292
+ return {
293
+ file,
294
+ status: "manual",
295
+ detail: "existing telemetry bootstrap needs the canonical Node guard and fail-closed host wrapper",
296
+ manualSnippet: INSTRUMENTATION_TS
297
+ };
246
298
  }
247
299
  const anchor = content.match(/export\s+async\s+function\s+register\s*\([^)]*\)\s*:\s*Promise<void>\s*\{/);
248
300
  if (!anchor || anchor.index === void 0) {
@@ -258,7 +310,13 @@ function patchInstrumentation(appDir, options) {
258
310
  // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.
259
311
  if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {
260
312
  const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')
261
- await registerTelemetryForNextjs()
313
+ try {
314
+ await registerTelemetryForNextjs()
315
+ } catch (err) {
316
+ const nodeProcess = process
317
+ nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)
318
+ nodeProcess.exit(1)
319
+ }
262
320
  }`;
263
321
  const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'
264
322
  `;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/telemetry-init.ts"],
4
- "sourcesContent": ["import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\n/**\n * `mercato telemetry init` \u2014 wire `@open-mercato/telemetry` into an app that was\n * scaffolded before telemetry existed. Deterministic, idempotent, and safe to\n * re-run: each step detects whether it is already applied and skips.\n *\n * The web-tier wiring lives in app-owned source files (a dependency bump cannot\n * touch them), so this patches them in place using the same idioms the rest of\n * the CLI uses: JSON edits, detect-before-append for `.env`, a ts-morph edit for\n * `next.config.ts`, and anchored insertion for `instrumentation.ts`, the API\n * dispatcher, `src/modules.ts`, and the backoffice layout. When a file's shape is\n * not recognized (a customized dispatcher, an unusual next.config, a rewritten\n * layout), the step degrades to printing the exact snippet and flags it as a\n * manual step rather than editing code it does not understand.\n *\n * \"Already wired\" is decided per capability, not per file: an app initialized\n * before browser RUM shipped has the core telemetry block and none of the RUM\n * wiring, and re-running the command is how it adopts the difference.\n *\n * Worker/scheduler telemetry is not handled here \u2014 it ships transitively via\n * `@open-mercato/cli`'s telemetry dependency once the app updates its packages.\n */\n\nconst BULLMQ_OTEL_VERSION = '^1.3.1'\n\ntype StepStatus = 'created' | 'patched' | 'skipped' | 'manual'\n\ntype StepResult = {\n file: string\n status: StepStatus\n detail: string\n /** Snippet to show when the step needs manual work. */\n manualSnippet?: string\n}\n\ntype TelemetryInitOptions = {\n dryRun: boolean\n}\n\nconst ENV_CORE_BLOCK = `\n# --- Telemetry & Observability (vendor-neutral OTLP: traces, logs, metrics, errors) ---\n# Off by default: leave TELEMETRY_BACKEND unset (or =noop) for a hard no-op\n# (the OpenTelemetry SDK is never loaded). Set to 'console' for local span/metric\n# output, or an OTLP backend to export: 'signoz' | 'newrelic' | 'otlp' (generic).\n# All three OTLP names use the same exporter \u2014 they differ only by the endpoint +\n# headers below. Any OTLP backend is a one-line swap; no code change.\n# TELEMETRY_BACKEND=otlp\n# Logs use the shared logger facade and its OM_LOG_LEVEL / OM_LOG_PRETTY /\n# OM_LOG_DESTINATION controls. Telemetry adds remote export; it does not create\n# a second local output path.\n# TELEMETRY_SAMPLING_RATIO=1.0 # 0.0\u20131.0 (default 1.0 dev / 0.1 prod)\n# TELEMETRY_TRUST_INBOUND_TRACE=false # default false: root-per-request (ignore an\n# # inbound traceparent from a proxy/LB). Set true\n# # only behind a trusted upstream whose trace continues.\n# # This also enables richer bullmq-otel spans;\n# # otherwise async queues use metadata._trace.\n#\n# OTLP endpoint + auth \u2014 pick ONE vendor (use your region's host):\n# SigNoz Cloud:\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<region>.signoz.cloud:443\n# OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<your-ingestion-key>\n# New Relic (OTLP-native; EU host shown, US is https://otlp.nr-data.net):\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.eu01.nr-data.net\n# OTEL_EXPORTER_OTLP_HEADERS=api-key=<your-ingest-license-key>\n#\n# Resource identity: keep one stable service name across environments and\n# separate envs via the deployment.environment resource attribute.\n# OTEL_SERVICE_NAME=open-mercato\n# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local\n`\n\n/**\n * Appended on its own when an app already carries the core block \u2014 an app\n * initialized before browser RUM shipped must be able to adopt it by re-running\n * the command.\n */\nconst ENV_BROWSER_BLOCK = `\n# --- Browser RUM (client-side telemetry) ---\n# Document-load and fetch spans exported from the backoffice through a\n# same-origin proxy (/api/telemetry/browser-traces) that adds the collector\n# credential server-side. Off by default; requires an active\n# TELEMETRY_BACKEND + OTLP endpoint above.\n# Read at request time (not NEXT_PUBLIC_*), so toggling needs no rebuild.\n# Browser spans join their server spans only when TELEMETRY_TRUST_INBOUND_TRACE=true\n# above; without it RUM still works, but each page yields two separate traces.\n# Ingest budget: each open tab exports at most 20 batches/min (a 3s batch delay),\n# and the proxy allows 400/min per authenticated user \u2014 roughly 20 concurrent tabs.\n# Over that, batches are dropped with 204 rather than a retryable status, so the\n# only symptom is missing spans for that one user, never a client retry storm.\n# TELEMETRY_BROWSER_ENABLED=false\n# TELEMETRY_BROWSER_SAMPLING_RATIO=1.0 # 0.0-1.0 (default 1.0)\n# TELEMETRY_BROWSER_SERVICE_NAME= # default: <OTEL_SERVICE_NAME>-browser\n`\n\nconst ENV_BLOCK = `${ENV_CORE_BLOCK}${ENV_BROWSER_BLOCK}`\n\nconst INSTRUMENTATION_TS = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n\nexport async function register(): Promise<void> {\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK\n // is Node-only and incompatible with the edge runtime, so the telemetry\n // bootstrap \u2014 which can pull in the SDK \u2014 is imported only on the Node.js\n // runtime. The helper owns init + graceful degrade + shutdown flush.\n if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')\n await registerTelemetryForNextjs()\n }\n}\n`\n\nconst DISPATCHER_SNIPPET = ` // add near the other imports:\n import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\n // in the dispatcher's success path, just before returning the response:\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\n\n // in the dispatcher's catch (error) block, before re-throwing:\n getTelemetryRuntime()?.reportError(error, {\n attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },\n })\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n\nfunction parseArgs(args: string[]): TelemetryInitOptions {\n return {\n dryRun: args.includes('--dry-run') || args.includes('-n'),\n }\n}\n\n/** Read the version an app already pins its `@open-mercato/*` deps to (lockstep). */\nfunction resolveOpenMercatoVersion(pkg: Record<string, unknown>): string {\n for (const field of ['dependencies', 'devDependencies'] as const) {\n const deps = pkg[field]\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(deps as Record<string, string>)) {\n if (name.startsWith('@open-mercato/') && typeof version === 'string') return version\n }\n }\n }\n return '*'\n}\n\nfunction patchPackageJson(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'package.json'\n const path = join(appDir, file)\n const raw = readFileSync(path, 'utf8')\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n }\n const version = resolveOpenMercatoVersion(pkg)\n const hasTelemetry = Boolean(pkg.dependencies?.['@open-mercato/telemetry'])\n const hasBullmqOtel = Boolean(pkg.optionalDependencies?.['bullmq-otel'])\n if (hasTelemetry && hasBullmqOtel) {\n return { file, status: 'skipped', detail: '@open-mercato/telemetry already installed' }\n }\n\n const deps = { ...(pkg.dependencies ?? {}) }\n if (!hasTelemetry) deps['@open-mercato/telemetry'] = version\n const optDeps = { ...(pkg.optionalDependencies ?? {}) }\n if (!hasBullmqOtel) optDeps['bullmq-otel'] = BULLMQ_OTEL_VERSION\n\n const next = {\n ...pkg,\n dependencies: sortRecord(deps),\n optionalDependencies: sortRecord(optDeps),\n }\n if (!options.dryRun) writeFileSync(path, `${JSON.stringify(next, null, 2)}\\n`)\n return {\n file,\n status: 'patched',\n detail: `+@open-mercato/telemetry@${version}${hasBullmqOtel ? '' : ` +bullmq-otel@${BULLMQ_OTEL_VERSION} (optional)`} \u2014 run \\`yarn install\\``,\n }\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)))\n}\n\nfunction patchEnvFile(appDir: string, relativePath: string, options: TelemetryInitOptions): StepResult | null {\n const path = join(appDir, relativePath)\n if (!existsSync(path)) return null\n const content = readFileSync(path, 'utf8')\n // Checked key by key, not block by block: an app initialized before browser RUM\n // shipped has the core keys and none of the browser ones, and re-running the\n // command is how it adopts them.\n const hasCore = content.includes('TELEMETRY_BACKEND')\n const hasBrowser = content.includes('TELEMETRY_BROWSER_ENABLED')\n if (hasCore && hasBrowser) {\n return { file: relativePath, status: 'skipped', detail: 'TELEMETRY_* block already present' }\n }\n const next = `${content.replace(/\\s*$/, '')}\\n${hasCore ? ENV_BROWSER_BLOCK : ENV_BLOCK}`\n if (!options.dryRun) writeFileSync(path, next)\n return {\n file: relativePath,\n status: 'patched',\n detail: hasCore\n ? 'appended the missing TELEMETRY_BROWSER_* block'\n : 'appended commented TELEMETRY_* / OTEL_* block',\n }\n}\n\nconst MODULES_SNIPPET = ` // in the enabledModules array:\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n\n/**\n * Register the `telemetry` module, which owns the same-origin OTLP proxy the\n * browser exporter posts to. Without it `TELEMETRY_BROWSER_ENABLED` produces a\n * client that exports into a 404.\n */\nfunction patchModules(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/modules.ts'\n const path = join(appDir, file)\n const content = readFileSync(path, 'utf8')\n if (/id:\\s*'telemetry'/.test(content)) {\n return { file, status: 'skipped', detail: 'telemetry module already enabled' }\n }\n const anchor = content.match(/export\\s+const\\s+enabledModules[^=]*=\\s*\\[/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'enabledModules array not found \u2014 add the entry by hand',\n manualSnippet: MODULES_SNIPPET,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Same-origin OTLP proxy for browser RUM spans; inert unless` +\n `\\n // TELEMETRY_BROWSER_ENABLED is set alongside an active telemetry backend.` +\n `\\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n if (!options.dryRun) writeFileSync(path, content.slice(0, insertAt) + block + content.slice(insertAt))\n return { file, status: 'patched', detail: \"enabled the 'telemetry' module (browser-traces proxy route)\" }\n}\n\nconst LAYOUT_FILE = 'src/app/(backend)/backend/layout.tsx'\n\nconst LAYOUT_SNIPPET = ` // add near the other imports:\n import { BrowserTelemetry } from '@open-mercato/telemetry/browser'\n import { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'\n\n // in the component body, before the returned JSX:\n // pass the request's Cookie header when the layout already reads cookies\n // (only consulted by the integration-test opt-in; production is env-only):\n const browserTelemetryConfig = resolveBrowserTelemetryConfig({ cookieHeader: cookieStore.toString() })\n\n // as the last child of <AppShell>:\n <BrowserTelemetry config={browserTelemetryConfig} />`\n\n/**\n * The config resolver takes the request's `Cookie` header so an integration spec can opt one page\n * into browser RUM; production resolution is env-only and never reads it. A scaffolded layout\n * already awaits `cookies()`, so reuse that binding \u2014 but an app that removed it must still get a\n * layout that compiles, hence the argument-free fallback. Both forms behave identically outside an\n * `OM_TEST_MODE` environment.\n */\nfunction resolveArgument(layout: string): string {\n const binding = layout.match(/const\\s+(\\w+)\\s*=\\s*await\\s+cookies\\(\\)/)\n return binding ? `{ cookieHeader: ${binding[1]}.toString() }` : ''\n}\n\n/**\n * Render the client bootstrap in the backoffice shell. The config is resolved\n * per request (the layout is `force-dynamic`), so RUM toggles per environment\n * without a rebuild.\n *\n * A backoffice layout is the file apps customize most, so this only edits one\n * that still matches the scaffold shape; anything else gets the snippet.\n */\nfunction patchBackendLayout(appDir: string, options: TelemetryInitOptions): StepResult {\n const path = join(appDir, LAYOUT_FILE)\n if (!existsSync(path)) {\n return { file: LAYOUT_FILE, status: 'manual', detail: 'backend layout not found', manualSnippet: LAYOUT_SNIPPET }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('<BrowserTelemetry')) {\n return { file: LAYOUT_FILE, status: 'skipped', detail: 'BrowserTelemetry already rendered' }\n }\n const closingShell = content.lastIndexOf('</AppShell>')\n const returnMatch = content.match(/^([ \\t]*)return \\(\\s*$/m)\n const lastImport = [...content.matchAll(/^import[^\\n]*$/gm)].at(-1)\n if (closingShell === -1 || !returnMatch || returnMatch.index === undefined || !lastImport) {\n return {\n file: LAYOUT_FILE,\n status: 'manual',\n detail: 'layout shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: LAYOUT_SNIPPET,\n }\n }\n\n // Edit back to front so each earlier anchor's offset stays valid.\n const shellIndent = content.slice(0, closingShell).match(/\\n([ \\t]*)$/)?.[1] ?? ' '\n let next =\n content.slice(0, closingShell) +\n `<BrowserTelemetry config={browserTelemetryConfig} />\\n${shellIndent}` +\n content.slice(closingShell)\n\n const returnIndent = returnMatch[1]\n next =\n next.slice(0, returnMatch.index) +\n `${returnIndent}// Resolved per request (this layout is force-dynamic), so browser RUM can be\\n` +\n `${returnIndent}// toggled per environment without a rebuild. Null keeps the SDK chunk from\\n` +\n `${returnIndent}// ever being requested.\\n` +\n `${returnIndent}const browserTelemetryConfig = resolveBrowserTelemetryConfig(${resolveArgument(content)})\\n\\n` +\n next.slice(returnMatch.index)\n\n const importInsertAt = (lastImport.index ?? 0) + lastImport[0].length\n next =\n next.slice(0, importInsertAt) +\n `\\nimport { BrowserTelemetry } from '@open-mercato/telemetry/browser'` +\n `\\nimport { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'` +\n next.slice(importInsertAt)\n\n if (!options.dryRun) writeFileSync(path, next)\n return { file: LAYOUT_FILE, status: 'patched', detail: 'rendered <BrowserTelemetry /> in the backoffice shell' }\n}\n\nfunction patchInstrumentation(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/instrumentation.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n if (!options.dryRun) {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, INSTRUMENTATION_TS)\n }\n return { file, status: 'created', detail: 'Node-runtime-guarded registerTelemetryForNextjs() bootstrap' }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('registerTelemetryForNextjs')) {\n if (content.includes('isTelemetryBackendEnabled')) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n const migrated =\n `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n` +\n content.replace(\n `if (process.env.NEXT_RUNTIME === 'nodejs') {`,\n `if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`,\n )\n if (!options.dryRun) writeFileSync(path, migrated)\n return { file, status: 'patched', detail: 'added the explicit backend import guard' }\n }\n const anchor = content.match(/export\\s+async\\s+function\\s+register\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*\\{/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'register() not found \u2014 add the telemetry bootstrap manually',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.` +\n `\\n if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {` +\n `\\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')` +\n `\\n await registerTelemetryForNextjs()` +\n `\\n }`\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withImport = content.includes('isTelemetryBackendEnabled')\n ? content\n : importLine + content\n const shiftedInsertAt = insertAt + (withImport.length - content.length)\n const next = withImport.slice(0, shiftedInsertAt) + block + withImport.slice(shiftedInsertAt)\n if (!options.dryRun) writeFileSync(path, next)\n return { file, status: 'patched', detail: 'inserted registerTelemetryForNextjs() into register()' }\n}\n\nasync function patchNextConfig(appDir: string, options: TelemetryInitOptions): Promise<StepResult> {\n const file = 'next.config.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'next.config.ts not found', manualSnippet: nextConfigSnippet() }\n }\n // Use ts-morph to *detect* the real `serverExternalPackages` array literal\n // (robust against a match inside a comment/string), then do the *insertion*\n // via a formatting-preserving text splice so the user's config stays tidy.\n const { Project, SyntaxKind } = await import('ts-morph')\n const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true })\n const sf = project.addSourceFileAtPath(path)\n\n const telemetryConfigImport = sf\n .getImportDeclarations()\n .find((decl) => [\n '@open-mercato/telemetry/nextjs',\n '@open-mercato/telemetry/nextjs-config',\n ].includes(decl.getModuleSpecifierValue()))\n const hasImport = Boolean(telemetryConfigImport)\n const hasCanonicalImport =\n telemetryConfigImport?.getModuleSpecifierValue() === '@open-mercato/telemetry/nextjs-config'\n\n const arrayProp = sf\n .getDescendantsOfKind(SyntaxKind.PropertyAssignment)\n .find((prop) => prop.getName() === 'serverExternalPackages')\n const arrayInit = arrayProp?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n if (!arrayInit) {\n return {\n file,\n status: 'manual',\n detail: 'serverExternalPackages array not found',\n manualSnippet: nextConfigSnippet(),\n }\n }\n const hasSpread = arrayInit.getElements().some((el) => el.getText().includes('telemetryServerExternalPackages'))\n if (hasCanonicalImport && hasSpread) {\n return { file, status: 'skipped', detail: 'telemetryServerExternalPackages already wired' }\n }\n\n let text = readFileSync(path, 'utf8')\n // Splice the array first, using the AST node's byte offsets (formatting-agnostic:\n // handles both single-line and multi-line arrays). Do this before modifying\n // imports, because either an inserted import or the legacy import migration\n // shifts the array offsets captured from the original source.\n if (!hasSpread) {\n const start = arrayInit.getStart()\n const end = arrayInit.getEnd()\n text = text.slice(0, start) + insertSpread(text.slice(start, end)) + text.slice(end)\n }\n text = text.replace(\n \"from '@open-mercato/telemetry/nextjs'\",\n \"from '@open-mercato/telemetry/nextjs-config'\",\n )\n if (!hasImport) {\n const importRegex = /^import[^\\n]*$/gm\n let lastImport: RegExpExecArray | null = null\n let match: RegExpExecArray | null\n while ((match = importRegex.exec(text)) !== null) lastImport = match\n const insertAt = lastImport ? lastImport.index + lastImport[0].length : 0\n text =\n text.slice(0, insertAt) +\n `\\nimport { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'` +\n text.slice(insertAt)\n }\n if (!options.dryRun) writeFileSync(path, text)\n return { file, status: 'patched', detail: 'spread telemetryServerExternalPackages into serverExternalPackages' }\n}\n\n/** Add `...telemetryServerExternalPackages` as the last element of an array literal, preserving its style. */\nfunction insertSpread(arrayText: string): string {\n const inner = arrayText.slice(1, -1)\n if (inner.includes('\\n')) {\n const elementIndent = inner.match(/\\n([ \\t]+)\\S/)?.[1] ?? ' '\n const closeIndent = arrayText.match(/\\n([ \\t]*)\\]$/)?.[1] ?? ''\n const trimmed = inner.replace(/\\s*$/, '')\n const separator = trimmed === '' || trimmed.endsWith(',') ? '' : ','\n return `[${trimmed}${separator}\\n${elementIndent}...telemetryServerExternalPackages,\\n${closeIndent}]`\n }\n const flat = inner.trim()\n return flat === '' ? '[...telemetryServerExternalPackages]' : `[${flat}, ...telemetryServerExternalPackages]`\n}\n\nfunction nextConfigSnippet(): string {\n return (\n `import { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'\\n` +\n `// serverExternalPackages: [ ...existing, ...telemetryServerExternalPackages ]`\n )\n}\n\n/**\n * Auto-patch the catch-all API dispatcher \u2014 but only when it still matches the\n * known scaffold shape (all anchors present, telemetry not yet wired). On any\n * mismatch, print the snippet and mark it manual rather than editing an\n * unrecognized (customized) handler.\n */\nfunction patchDispatcher(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/app/api/[...slug]/route.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'dispatcher not found', manualSnippet: DISPATCHER_SNIPPET }\n }\n let content = readFileSync(path, 'utf8')\n if (\n content.includes(\"from '@open-mercato/shared/lib/telemetry/runtime'\")\n && content.includes('getTelemetryRuntime()?.recordHttpDuration')\n ) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n if (\n content.includes(\"import { reportError } from '@open-mercato/telemetry'\")\n && content.includes(\"import { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\")\n ) {\n content = content\n .replace(\n \"import { reportError } from '@open-mercato/telemetry'\\nimport { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\",\n \"import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\",\n )\n .replace(/\\brecordHttpDuration\\(/g, 'getTelemetryRuntime()?.recordHttpDuration(')\n .replace(/\\breportError\\(/g, 'getTelemetryRuntime()?.reportError(')\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'migrated dispatcher to the default-off runtime bridge' }\n }\n\n const importAnchor = \"import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\"\n const returnMatch = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n // The handleRequest catch is the first `} catch (error) {` after the success return.\n const catchRegex = /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm\n const catchMatch = findCatchAfter(content, catchRegex, returnMatch?.index ?? -1)\n\n if (!content.includes(importAnchor) || !returnMatch || returnMatch.index === undefined || !catchMatch) {\n return {\n file,\n status: 'manual',\n detail: 'dispatcher shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: DISPATCHER_SNIPPET,\n }\n }\n\n // 1) imports\n content = content.replace(\n importAnchor,\n `${importAnchor}\\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'`,\n )\n\n // 2) success-path metric (re-match after the import edit shifted offsets)\n const ret = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n if (!ret || ret.index === undefined) {\n return { file, status: 'manual', detail: 'success-path anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const retIndent = ret[1]\n content =\n content.slice(0, ret.index) +\n `${retIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\\n` +\n content.slice(ret.index)\n\n // 3) catch-block error funnel + 500 metric\n const catch2 = findCatchAfter(content, /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm, content.indexOf('return finalResponse'))\n if (!catch2) {\n return { file, status: 'manual', detail: 'catch anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const bodyIndent = `${catch2.indent} `\n const insertAt = catch2.index + catch2.length\n const block =\n `\\n${bodyIndent}getTelemetryRuntime()?.reportError(error, {` +\n `\\n${bodyIndent} attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },` +\n `\\n${bodyIndent}})` +\n `\\n${bodyIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n content = content.slice(0, insertAt) + block + content.slice(insertAt)\n\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'wired reportError (5xx) + http.server.request.duration metric' }\n}\n\nfunction findCatchAfter(\n content: string,\n regex: RegExp,\n afterIndex: number,\n): { index: number; length: number; indent: string } | null {\n regex.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = regex.exec(content)) !== null) {\n if (match.index > afterIndex) {\n return { index: match.index, length: match[0].length, indent: match[1] }\n }\n }\n return null\n}\n\nexport async function runTelemetryInit(args: string[]): Promise<number> {\n const appDir = resolve('.')\n const options = parseArgs(args)\n\n if (!existsSync(join(appDir, 'src', 'modules.ts'))) {\n console.error('\u274C Not an Open Mercato app directory (src/modules.ts not found)')\n return 1\n }\n if (!existsSync(join(appDir, 'package.json'))) {\n console.error('\u274C package.json not found')\n return 1\n }\n\n console.log('')\n console.log(options.dryRun ? '\uD83D\uDD2D Telemetry init (dry run \u2014 no files written)\\n' : '\uD83D\uDD2D Wiring @open-mercato/telemetry into this app\\n')\n\n const results: StepResult[] = []\n results.push(patchPackageJson(appDir, options))\n const envExample = patchEnvFile(appDir, '.env.example', options)\n if (envExample) results.push(envExample)\n const env = patchEnvFile(appDir, '.env', options)\n if (env) results.push(env)\n results.push(patchInstrumentation(appDir, options))\n results.push(await patchNextConfig(appDir, options))\n results.push(patchDispatcher(appDir, options))\n results.push(patchModules(appDir, options))\n results.push(patchBackendLayout(appDir, options))\n\n const icon: Record<StepStatus, string> = { created: '\u270D\uFE0F ', patched: '\u2705', skipped: '\u23ED\uFE0F ', manual: '\u26A0\uFE0F ' }\n for (const result of results) {\n console.log(`${icon[result.status]} ${result.file} \u2014 ${result.detail}`)\n }\n\n const manual = results.filter((result) => result.status === 'manual')\n for (const result of manual) {\n if (!result.manualSnippet) continue\n console.log('')\n console.log(`\u2500\u2500 manual step for ${result.file} \u2500\u2500`)\n console.log(result.manualSnippet)\n }\n\n console.log('')\n const changed = results.some((result) => result.status === 'patched' || result.status === 'created')\n if (options.dryRun) {\n console.log('Dry run complete \u2014 re-run without --dry-run to apply.')\n } else if (changed) {\n console.log('Next steps:')\n console.log(' 1. yarn install (resolve @open-mercato/telemetry + the OTEL SDK)')\n console.log(' 2. set TELEMETRY_BACKEND + OTEL_EXPORTER_OTLP_* in .env')\n console.log(' 3. yarn typecheck && yarn build (verify the wiring)')\n } else {\n console.log('Nothing to do \u2014 telemetry is already wired.')\n }\n if (manual.length > 0) {\n console.log('')\n console.log(`\u26A0\uFE0F ${manual.length} file(s) need a manual edit (shown above) \u2014 the rest were applied automatically.`)\n }\n return 0\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\n/**\n * `mercato telemetry init` \u2014 wire `@open-mercato/telemetry` into an app that was\n * scaffolded before telemetry existed. Deterministic, idempotent, and safe to\n * re-run: each step detects whether it is already applied and skips.\n *\n * The web-tier wiring lives in app-owned source files (a dependency bump cannot\n * touch them), so this patches them in place using the same idioms the rest of\n * the CLI uses: JSON edits, detect-before-append for `.env`, a ts-morph edit for\n * `next.config.ts`, and anchored insertion for `instrumentation.ts`, the API\n * dispatcher, `src/modules.ts`, and the backoffice layout. When a file's shape is\n * not recognized (a customized dispatcher, an unusual next.config, a rewritten\n * layout), the step degrades to printing the exact snippet and flags it as a\n * manual step rather than editing code it does not understand.\n *\n * \"Already wired\" is decided per capability, not per file: an app initialized\n * before browser RUM shipped has the core telemetry block and none of the RUM\n * wiring, and re-running the command is how it adopts the difference.\n *\n * Worker/scheduler telemetry is not handled here \u2014 it ships transitively via\n * `@open-mercato/cli`'s telemetry dependency once the app updates its packages.\n */\n\nconst BULLMQ_OTEL_VERSION = '^1.3.1'\n\ntype StepStatus = 'created' | 'patched' | 'skipped' | 'manual'\n\ntype StepResult = {\n file: string\n status: StepStatus\n detail: string\n /** Snippet to show when the step needs manual work. */\n manualSnippet?: string\n}\n\ntype TelemetryInitOptions = {\n dryRun: boolean\n}\n\nconst ENV_CORE_BLOCK = `\n# --- Telemetry & Observability (vendor-neutral OTLP: traces, logs, metrics, errors) ---\n# Off by default: leave TELEMETRY_BACKEND unset (or =noop) for a hard no-op\n# (the OpenTelemetry SDK is never loaded). Set to 'console' for local span/metric\n# output, or an OTLP backend to export: 'signoz' | 'newrelic' | 'otlp' (generic).\n# All three OTLP names use the same exporter \u2014 they differ only by the endpoint +\n# headers below. Any OTLP backend is a one-line swap; no code change.\n# TELEMETRY_BACKEND=otlp\n# Logs use the shared logger facade and its OM_LOG_LEVEL / OM_LOG_PRETTY /\n# OM_LOG_DESTINATION controls. Telemetry adds remote export; it does not create\n# a second local output path.\n# TELEMETRY_SAMPLING_RATIO=1.0 # 0.0\u20131.0 (default 1.0 dev / 0.1 prod)\n# TELEMETRY_TRUST_INBOUND_TRACE=false # default false: root-per-request (ignore an\n# # inbound traceparent from a proxy/LB). Set true\n# # only behind a trusted upstream whose trace continues.\n# # This also enables richer bullmq-otel spans;\n# # otherwise async queues use metadata._trace.\n#\n# OTLP endpoint + auth \u2014 pick ONE vendor (use your region's host):\n# SigNoz Cloud:\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.<region>.signoz.cloud:443\n# OTEL_EXPORTER_OTLP_HEADERS=signoz-ingestion-key=<your-ingestion-key>\n# New Relic (OTLP-native; EU host shown, US is https://otlp.nr-data.net):\n# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.eu01.nr-data.net\n# OTEL_EXPORTER_OTLP_HEADERS=api-key=<your-ingest-license-key>\n#\n# Resource identity: keep one stable service name across environments and\n# separate envs via the deployment.environment resource attribute.\n# OTEL_SERVICE_NAME=open-mercato\n# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local\n`\n\n/**\n * Appended on its own when an app already carries the core block \u2014 an app\n * initialized before browser RUM shipped must be able to adopt it by re-running\n * the command.\n */\nconst ENV_BROWSER_BLOCK = `\n# --- Browser RUM (client-side telemetry) ---\n# Document-load and fetch spans exported from the backoffice through a\n# same-origin proxy (/api/telemetry/browser-traces) that adds the collector\n# credential server-side. Off by default; requires an active\n# TELEMETRY_BACKEND + OTLP endpoint above.\n# Read at request time (not NEXT_PUBLIC_*), so toggling needs no rebuild.\n# Browser spans join their server spans only when TELEMETRY_TRUST_INBOUND_TRACE=true\n# above; without it RUM still works, but each page yields two separate traces.\n# Ingest budget: each open tab exports at most 20 batches/min (a 3s batch delay),\n# and the proxy allows 400/min per authenticated user \u2014 roughly 20 concurrent tabs.\n# Over that, batches are dropped with 204 rather than a retryable status, so the\n# only symptom is missing spans for that one user, never a client retry storm.\n# TELEMETRY_BROWSER_ENABLED=false\n# TELEMETRY_BROWSER_SAMPLING_RATIO=1.0 # 0.0-1.0 (default 1.0)\n# TELEMETRY_BROWSER_SERVICE_NAME= # default: <OTEL_SERVICE_NAME>-browser\n`\n\nconst ENV_BLOCK = `${ENV_CORE_BLOCK}${ENV_BROWSER_BLOCK}`\n\nconst INSTRUMENTATION_TS = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n\nexport async function register(): Promise<void> {\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK\n // is Node-only and incompatible with the edge runtime, so the telemetry\n // bootstrap \u2014 which can pull in the SDK \u2014 is imported only on the Node.js\n // runtime. The helper owns init + graceful degrade + shutdown flush; a\n // deliberate configuration failure bubbles here so the host exits loudly.\n if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')\n try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }\n }\n}\n`\n\nconst MULTILINE_TELEMETRY_GUARD = ` if (\n process.env.NEXT_RUNTIME === 'nodejs'\n && isTelemetryBackendEnabled()\n ) {`\nconst INLINE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`\nconst LEGACY_NODE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs') {`\nconst TELEMETRY_DYNAMIC_IMPORT = ` const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')`\nconst BARE_TELEMETRY_REGISTRATION = ` await registerTelemetryForNextjs()`\nconst GUARDED_TELEMETRY_REGISTRATION = ` try {\n await registerTelemetryForNextjs()\n } catch (err) {\n const nodeProcess = process\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)\n nodeProcess.exit(1)\n }`\n\nconst DISPATCHER_SNIPPET = ` // add near the other imports:\n import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\n\n // in the dispatcher's success path, just before returning the response:\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\n\n // in the dispatcher's catch (error) block, before re-throwing:\n getTelemetryRuntime()?.reportError(error, {\n attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },\n })\n getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n\nfunction parseArgs(args: string[]): TelemetryInitOptions {\n return {\n dryRun: args.includes('--dry-run') || args.includes('-n'),\n }\n}\n\n/** Read the version an app already pins its `@open-mercato/*` deps to (lockstep). */\nfunction resolveOpenMercatoVersion(pkg: Record<string, unknown>): string {\n for (const field of ['dependencies', 'devDependencies'] as const) {\n const deps = pkg[field]\n if (deps && typeof deps === 'object') {\n for (const [name, version] of Object.entries(deps as Record<string, string>)) {\n if (name.startsWith('@open-mercato/') && typeof version === 'string') return version\n }\n }\n }\n return '*'\n}\n\nfunction patchPackageJson(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'package.json'\n const path = join(appDir, file)\n const raw = readFileSync(path, 'utf8')\n const pkg = JSON.parse(raw) as {\n dependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n }\n const version = resolveOpenMercatoVersion(pkg)\n const hasTelemetry = Boolean(pkg.dependencies?.['@open-mercato/telemetry'])\n const hasBullmqOtel = Boolean(pkg.optionalDependencies?.['bullmq-otel'])\n if (hasTelemetry && hasBullmqOtel) {\n return { file, status: 'skipped', detail: '@open-mercato/telemetry already installed' }\n }\n\n const deps = { ...(pkg.dependencies ?? {}) }\n if (!hasTelemetry) deps['@open-mercato/telemetry'] = version\n const optDeps = { ...(pkg.optionalDependencies ?? {}) }\n if (!hasBullmqOtel) optDeps['bullmq-otel'] = BULLMQ_OTEL_VERSION\n\n const next = {\n ...pkg,\n dependencies: sortRecord(deps),\n optionalDependencies: sortRecord(optDeps),\n }\n if (!options.dryRun) writeFileSync(path, `${JSON.stringify(next, null, 2)}\\n`)\n return {\n file,\n status: 'patched',\n detail: `+@open-mercato/telemetry@${version}${hasBullmqOtel ? '' : ` +bullmq-otel@${BULLMQ_OTEL_VERSION} (optional)`} \u2014 run \\`yarn install\\``,\n }\n}\n\nfunction sortRecord(record: Record<string, string>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)))\n}\n\nfunction patchEnvFile(appDir: string, relativePath: string, options: TelemetryInitOptions): StepResult | null {\n const path = join(appDir, relativePath)\n if (!existsSync(path)) return null\n const content = readFileSync(path, 'utf8')\n // Checked key by key, not block by block: an app initialized before browser RUM\n // shipped has the core keys and none of the browser ones, and re-running the\n // command is how it adopts them.\n const hasCore = content.includes('TELEMETRY_BACKEND')\n const hasBrowser = content.includes('TELEMETRY_BROWSER_ENABLED')\n if (hasCore && hasBrowser) {\n return { file: relativePath, status: 'skipped', detail: 'TELEMETRY_* block already present' }\n }\n const next = `${content.replace(/\\s*$/, '')}\\n${hasCore ? ENV_BROWSER_BLOCK : ENV_BLOCK}`\n if (!options.dryRun) writeFileSync(path, next)\n return {\n file: relativePath,\n status: 'patched',\n detail: hasCore\n ? 'appended the missing TELEMETRY_BROWSER_* block'\n : 'appended commented TELEMETRY_* / OTEL_* block',\n }\n}\n\nconst MODULES_SNIPPET = ` // in the enabledModules array:\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n\n/**\n * Register the `telemetry` module, which owns the same-origin OTLP proxy the\n * browser exporter posts to. Without it `TELEMETRY_BROWSER_ENABLED` produces a\n * client that exports into a 404.\n */\nfunction patchModules(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/modules.ts'\n const path = join(appDir, file)\n const content = readFileSync(path, 'utf8')\n if (/id:\\s*'telemetry'/.test(content)) {\n return { file, status: 'skipped', detail: 'telemetry module already enabled' }\n }\n const anchor = content.match(/export\\s+const\\s+enabledModules[^=]*=\\s*\\[/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'enabledModules array not found \u2014 add the entry by hand',\n manualSnippet: MODULES_SNIPPET,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Same-origin OTLP proxy for browser RUM spans; inert unless` +\n `\\n // TELEMETRY_BROWSER_ENABLED is set alongside an active telemetry backend.` +\n `\\n { id: 'telemetry', from: '@open-mercato/telemetry' },`\n if (!options.dryRun) writeFileSync(path, content.slice(0, insertAt) + block + content.slice(insertAt))\n return { file, status: 'patched', detail: \"enabled the 'telemetry' module (browser-traces proxy route)\" }\n}\n\nconst LAYOUT_FILE = 'src/app/(backend)/backend/layout.tsx'\n\nconst LAYOUT_SNIPPET = ` // add near the other imports:\n import { BrowserTelemetry } from '@open-mercato/telemetry/browser'\n import { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'\n\n // in the component body, before the returned JSX:\n // pass the request's Cookie header when the layout already reads cookies\n // (only consulted by the integration-test opt-in; production is env-only):\n const browserTelemetryConfig = resolveBrowserTelemetryConfig({ cookieHeader: cookieStore.toString() })\n\n // as the last child of <AppShell>:\n <BrowserTelemetry config={browserTelemetryConfig} />`\n\n/**\n * The config resolver takes the request's `Cookie` header so an integration spec can opt one page\n * into browser RUM; production resolution is env-only and never reads it. A scaffolded layout\n * already awaits `cookies()`, so reuse that binding \u2014 but an app that removed it must still get a\n * layout that compiles, hence the argument-free fallback. Both forms behave identically outside an\n * `OM_TEST_MODE` environment.\n */\nfunction resolveArgument(layout: string): string {\n const binding = layout.match(/const\\s+(\\w+)\\s*=\\s*await\\s+cookies\\(\\)/)\n return binding ? `{ cookieHeader: ${binding[1]}.toString() }` : ''\n}\n\n/**\n * Render the client bootstrap in the backoffice shell. The config is resolved\n * per request (the layout is `force-dynamic`), so RUM toggles per environment\n * without a rebuild.\n *\n * A backoffice layout is the file apps customize most, so this only edits one\n * that still matches the scaffold shape; anything else gets the snippet.\n */\nfunction patchBackendLayout(appDir: string, options: TelemetryInitOptions): StepResult {\n const path = join(appDir, LAYOUT_FILE)\n if (!existsSync(path)) {\n return { file: LAYOUT_FILE, status: 'manual', detail: 'backend layout not found', manualSnippet: LAYOUT_SNIPPET }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('<BrowserTelemetry')) {\n return { file: LAYOUT_FILE, status: 'skipped', detail: 'BrowserTelemetry already rendered' }\n }\n const closingShell = content.lastIndexOf('</AppShell>')\n const returnMatch = content.match(/^([ \\t]*)return \\(\\s*$/m)\n const lastImport = [...content.matchAll(/^import[^\\n]*$/gm)].at(-1)\n if (closingShell === -1 || !returnMatch || returnMatch.index === undefined || !lastImport) {\n return {\n file: LAYOUT_FILE,\n status: 'manual',\n detail: 'layout shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: LAYOUT_SNIPPET,\n }\n }\n\n // Edit back to front so each earlier anchor's offset stays valid.\n const shellIndent = content.slice(0, closingShell).match(/\\n([ \\t]*)$/)?.[1] ?? ' '\n let next =\n content.slice(0, closingShell) +\n `<BrowserTelemetry config={browserTelemetryConfig} />\\n${shellIndent}` +\n content.slice(closingShell)\n\n const returnIndent = returnMatch[1]\n next =\n next.slice(0, returnMatch.index) +\n `${returnIndent}// Resolved per request (this layout is force-dynamic), so browser RUM can be\\n` +\n `${returnIndent}// toggled per environment without a rebuild. Null keeps the SDK chunk from\\n` +\n `${returnIndent}// ever being requested.\\n` +\n `${returnIndent}const browserTelemetryConfig = resolveBrowserTelemetryConfig(${resolveArgument(content)})\\n\\n` +\n next.slice(returnMatch.index)\n\n const importInsertAt = (lastImport.index ?? 0) + lastImport[0].length\n next =\n next.slice(0, importInsertAt) +\n `\\nimport { BrowserTelemetry } from '@open-mercato/telemetry/browser'` +\n `\\nimport { resolveBrowserTelemetryConfig } from '@open-mercato/telemetry/browser/server'` +\n next.slice(importInsertAt)\n\n if (!options.dryRun) writeFileSync(path, next)\n return { file: LAYOUT_FILE, status: 'patched', detail: 'rendered <BrowserTelemetry /> in the backoffice shell' }\n}\n\nfunction patchInstrumentation(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/instrumentation.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n if (!options.dryRun) {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, INSTRUMENTATION_TS)\n }\n return { file, status: 'created', detail: 'Node-runtime-guarded registerTelemetryForNextjs() bootstrap' }\n }\n const content = readFileSync(path, 'utf8')\n if (content.includes('registerTelemetryForNextjs')) {\n const guardedBootstraps = [MULTILINE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD]\n const safeBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${GUARDED_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (safeBootstrap) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n const guardedLegacyBootstrap = guardedBootstraps\n .map((guard) => `${guard}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`)\n .find((bootstrap) => content.includes(bootstrap))\n if (guardedLegacyBootstrap) {\n const migrated = content.replace(\n guardedLegacyBootstrap,\n guardedLegacyBootstrap.replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION),\n )\n if (!options.dryRun) writeFileSync(path, migrated)\n return { file, status: 'patched', detail: 'wrapped OTLP dependency failures with host termination' }\n }\n const nodeOnlyLegacyBootstrap = `${LEGACY_NODE_TELEMETRY_GUARD}\\n${TELEMETRY_DYNAMIC_IMPORT}\\n${BARE_TELEMETRY_REGISTRATION}`\n if (content.includes(nodeOnlyLegacyBootstrap)) {\n const guardedBootstrap = nodeOnlyLegacyBootstrap\n .replace(LEGACY_NODE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD)\n .replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION)\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withBootstrap = content.replace(nodeOnlyLegacyBootstrap, guardedBootstrap)\n const migrated = withBootstrap.includes(importLine) ? withBootstrap : importLine + withBootstrap\n if (!options.dryRun) writeFileSync(path, migrated)\n return {\n file,\n status: 'patched',\n detail: 'added the explicit backend guard and wrapped OTLP dependency failures with host termination',\n }\n }\n return {\n file,\n status: 'manual',\n detail: 'existing telemetry bootstrap needs the canonical Node guard and fail-closed host wrapper',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const anchor = content.match(/export\\s+async\\s+function\\s+register\\s*\\([^)]*\\)\\s*:\\s*Promise<void>\\s*\\{/)\n if (!anchor || anchor.index === undefined) {\n return {\n file,\n status: 'manual',\n detail: 'register() not found \u2014 add the telemetry bootstrap manually',\n manualSnippet: INSTRUMENTATION_TS,\n }\n }\n const insertAt = anchor.index + anchor[0].length\n const block =\n `\\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.` +\n `\\n if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {` +\n `\\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')` +\n `\\n try {` +\n `\\n await registerTelemetryForNextjs()` +\n `\\n } catch (err) {` +\n `\\n const nodeProcess = process` +\n `\\n nodeProcess.stderr.write(\\`\\${err instanceof Error ? err.message : String(err)}\\\\n\\`)` +\n `\\n nodeProcess.exit(1)` +\n `\\n }` +\n `\\n }`\n const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\\n`\n const withImport = content.includes('isTelemetryBackendEnabled')\n ? content\n : importLine + content\n const shiftedInsertAt = insertAt + (withImport.length - content.length)\n const next = withImport.slice(0, shiftedInsertAt) + block + withImport.slice(shiftedInsertAt)\n if (!options.dryRun) writeFileSync(path, next)\n return { file, status: 'patched', detail: 'inserted registerTelemetryForNextjs() into register()' }\n}\n\nasync function patchNextConfig(appDir: string, options: TelemetryInitOptions): Promise<StepResult> {\n const file = 'next.config.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'next.config.ts not found', manualSnippet: nextConfigSnippet() }\n }\n // Use ts-morph to *detect* the real `serverExternalPackages` array literal\n // (robust against a match inside a comment/string), then do the *insertion*\n // via a formatting-preserving text splice so the user's config stays tidy.\n const { Project, SyntaxKind } = await import('ts-morph')\n const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true })\n const sf = project.addSourceFileAtPath(path)\n\n const telemetryConfigImport = sf\n .getImportDeclarations()\n .find((decl) => [\n '@open-mercato/telemetry/nextjs',\n '@open-mercato/telemetry/nextjs-config',\n ].includes(decl.getModuleSpecifierValue()))\n const hasImport = Boolean(telemetryConfigImport)\n const hasCanonicalImport =\n telemetryConfigImport?.getModuleSpecifierValue() === '@open-mercato/telemetry/nextjs-config'\n\n const arrayProp = sf\n .getDescendantsOfKind(SyntaxKind.PropertyAssignment)\n .find((prop) => prop.getName() === 'serverExternalPackages')\n const arrayInit = arrayProp?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n if (!arrayInit) {\n return {\n file,\n status: 'manual',\n detail: 'serverExternalPackages array not found',\n manualSnippet: nextConfigSnippet(),\n }\n }\n const hasSpread = arrayInit.getElements().some((el) => el.getText().includes('telemetryServerExternalPackages'))\n if (hasCanonicalImport && hasSpread) {\n return { file, status: 'skipped', detail: 'telemetryServerExternalPackages already wired' }\n }\n\n let text = readFileSync(path, 'utf8')\n // Splice the array first, using the AST node's byte offsets (formatting-agnostic:\n // handles both single-line and multi-line arrays). Do this before modifying\n // imports, because either an inserted import or the legacy import migration\n // shifts the array offsets captured from the original source.\n if (!hasSpread) {\n const start = arrayInit.getStart()\n const end = arrayInit.getEnd()\n text = text.slice(0, start) + insertSpread(text.slice(start, end)) + text.slice(end)\n }\n text = text.replace(\n \"from '@open-mercato/telemetry/nextjs'\",\n \"from '@open-mercato/telemetry/nextjs-config'\",\n )\n if (!hasImport) {\n const importRegex = /^import[^\\n]*$/gm\n let lastImport: RegExpExecArray | null = null\n let match: RegExpExecArray | null\n while ((match = importRegex.exec(text)) !== null) lastImport = match\n const insertAt = lastImport ? lastImport.index + lastImport[0].length : 0\n text =\n text.slice(0, insertAt) +\n `\\nimport { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'` +\n text.slice(insertAt)\n }\n if (!options.dryRun) writeFileSync(path, text)\n return { file, status: 'patched', detail: 'spread telemetryServerExternalPackages into serverExternalPackages' }\n}\n\n/** Add `...telemetryServerExternalPackages` as the last element of an array literal, preserving its style. */\nfunction insertSpread(arrayText: string): string {\n const inner = arrayText.slice(1, -1)\n if (inner.includes('\\n')) {\n const elementIndent = inner.match(/\\n([ \\t]+)\\S/)?.[1] ?? ' '\n const closeIndent = arrayText.match(/\\n([ \\t]*)\\]$/)?.[1] ?? ''\n const trimmed = inner.replace(/\\s*$/, '')\n const separator = trimmed === '' || trimmed.endsWith(',') ? '' : ','\n return `[${trimmed}${separator}\\n${elementIndent}...telemetryServerExternalPackages,\\n${closeIndent}]`\n }\n const flat = inner.trim()\n return flat === '' ? '[...telemetryServerExternalPackages]' : `[${flat}, ...telemetryServerExternalPackages]`\n}\n\nfunction nextConfigSnippet(): string {\n return (\n `import { telemetryServerExternalPackages } from '@open-mercato/telemetry/nextjs-config'\\n` +\n `// serverExternalPackages: [ ...existing, ...telemetryServerExternalPackages ]`\n )\n}\n\n/**\n * Auto-patch the catch-all API dispatcher \u2014 but only when it still matches the\n * known scaffold shape (all anchors present, telemetry not yet wired). On any\n * mismatch, print the snippet and mark it manual rather than editing an\n * unrecognized (customized) handler.\n */\nfunction patchDispatcher(appDir: string, options: TelemetryInitOptions): StepResult {\n const file = 'src/app/api/[...slug]/route.ts'\n const path = join(appDir, file)\n if (!existsSync(path)) {\n return { file, status: 'manual', detail: 'dispatcher not found', manualSnippet: DISPATCHER_SNIPPET }\n }\n let content = readFileSync(path, 'utf8')\n if (\n content.includes(\"from '@open-mercato/shared/lib/telemetry/runtime'\")\n && content.includes('getTelemetryRuntime()?.recordHttpDuration')\n ) {\n return { file, status: 'skipped', detail: 'telemetry already wired' }\n }\n if (\n content.includes(\"import { reportError } from '@open-mercato/telemetry'\")\n && content.includes(\"import { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\")\n ) {\n content = content\n .replace(\n \"import { reportError } from '@open-mercato/telemetry'\\nimport { recordHttpDuration } from '@open-mercato/telemetry/nextjs'\",\n \"import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\",\n )\n .replace(/\\brecordHttpDuration\\(/g, 'getTelemetryRuntime()?.recordHttpDuration(')\n .replace(/\\breportError\\(/g, 'getTelemetryRuntime()?.reportError(')\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'migrated dispatcher to the default-off runtime bridge' }\n }\n\n const importAnchor = \"import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\"\n const returnMatch = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n // The handleRequest catch is the first `} catch (error) {` after the success return.\n const catchRegex = /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm\n const catchMatch = findCatchAfter(content, catchRegex, returnMatch?.index ?? -1)\n\n if (!content.includes(importAnchor) || !returnMatch || returnMatch.index === undefined || !catchMatch) {\n return {\n file,\n status: 'manual',\n detail: 'dispatcher shape not recognized \u2014 apply the snippet by hand',\n manualSnippet: DISPATCHER_SNIPPET,\n }\n }\n\n // 1) imports\n content = content.replace(\n importAnchor,\n `${importAnchor}\\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'`,\n )\n\n // 2) success-path metric (re-match after the import edit shifted offsets)\n const ret = content.match(/^([ \\t]*)return finalResponse\\s*$/m)\n if (!ret || ret.index === undefined) {\n return { file, status: 'manual', detail: 'success-path anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const retIndent = ret[1]\n content =\n content.slice(0, ret.index) +\n `${retIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, finalResponse.status, startedAt)\\n` +\n content.slice(ret.index)\n\n // 3) catch-block error funnel + 500 metric\n const catch2 = findCatchAfter(content, /^([ \\t]*)\\}\\s*catch\\s*\\(error\\)\\s*\\{[ \\t]*$/gm, content.indexOf('return finalResponse'))\n if (!catch2) {\n return { file, status: 'manual', detail: 'catch anchor lost', manualSnippet: DISPATCHER_SNIPPET }\n }\n const bodyIndent = `${catch2.indent} `\n const insertAt = catch2.index + catch2.length\n const block =\n `\\n${bodyIndent}getTelemetryRuntime()?.reportError(error, {` +\n `\\n${bodyIndent} attributes: { 'http.request.method': method, 'http.route': match.route.path, 'http.response.status_code': 500 },` +\n `\\n${bodyIndent}})` +\n `\\n${bodyIndent}getTelemetryRuntime()?.recordHttpDuration(method, match.route.path, 500, startedAt)`\n content = content.slice(0, insertAt) + block + content.slice(insertAt)\n\n if (!options.dryRun) writeFileSync(path, content)\n return { file, status: 'patched', detail: 'wired reportError (5xx) + http.server.request.duration metric' }\n}\n\nfunction findCatchAfter(\n content: string,\n regex: RegExp,\n afterIndex: number,\n): { index: number; length: number; indent: string } | null {\n regex.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = regex.exec(content)) !== null) {\n if (match.index > afterIndex) {\n return { index: match.index, length: match[0].length, indent: match[1] }\n }\n }\n return null\n}\n\nexport async function runTelemetryInit(args: string[]): Promise<number> {\n const appDir = resolve('.')\n const options = parseArgs(args)\n\n if (!existsSync(join(appDir, 'src', 'modules.ts'))) {\n console.error('\u274C Not an Open Mercato app directory (src/modules.ts not found)')\n return 1\n }\n if (!existsSync(join(appDir, 'package.json'))) {\n console.error('\u274C package.json not found')\n return 1\n }\n\n console.log('')\n console.log(options.dryRun ? '\uD83D\uDD2D Telemetry init (dry run \u2014 no files written)\\n' : '\uD83D\uDD2D Wiring @open-mercato/telemetry into this app\\n')\n\n const results: StepResult[] = []\n results.push(patchPackageJson(appDir, options))\n const envExample = patchEnvFile(appDir, '.env.example', options)\n if (envExample) results.push(envExample)\n const env = patchEnvFile(appDir, '.env', options)\n if (env) results.push(env)\n results.push(patchInstrumentation(appDir, options))\n results.push(await patchNextConfig(appDir, options))\n results.push(patchDispatcher(appDir, options))\n results.push(patchModules(appDir, options))\n results.push(patchBackendLayout(appDir, options))\n\n const icon: Record<StepStatus, string> = { created: '\u270D\uFE0F ', patched: '\u2705', skipped: '\u23ED\uFE0F ', manual: '\u26A0\uFE0F ' }\n for (const result of results) {\n console.log(`${icon[result.status]} ${result.file} \u2014 ${result.detail}`)\n }\n\n const manual = results.filter((result) => result.status === 'manual')\n for (const result of manual) {\n if (!result.manualSnippet) continue\n console.log('')\n console.log(`\u2500\u2500 manual step for ${result.file} \u2500\u2500`)\n console.log(result.manualSnippet)\n }\n\n console.log('')\n const changed = results.some((result) => result.status === 'patched' || result.status === 'created')\n if (options.dryRun) {\n console.log('Dry run complete \u2014 re-run without --dry-run to apply.')\n } else if (changed) {\n console.log('Next steps:')\n console.log(' 1. yarn install (resolve @open-mercato/telemetry + the OTEL SDK)')\n console.log(' 2. set TELEMETRY_BACKEND + OTEL_EXPORTER_OTLP_* in .env')\n console.log(' 3. yarn typecheck && yarn build (verify the wiring)')\n } else {\n console.log('Nothing to do \u2014 telemetry is already wired.')\n }\n if (manual.length > 0) {\n console.log('')\n console.log(`\u26A0\uFE0F ${manual.length} file(s) need a manual edit (shown above) \u2014 the rest were applied automatically.`)\n }\n return 0\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["importLine"]
7
7
  }
@@ -8,6 +8,7 @@ import { createInterface } from "node:readline/promises";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
  import spawn from "cross-spawn";
10
10
  import { fetchWithTimeout } from "@open-mercato/shared/lib/http/fetchWithTimeout";
11
+ import { isUnsafeJwtSecret } from "@open-mercato/shared/lib/auth/jwt";
11
12
  import { resolveEnvironment } from "../resolver.js";
12
13
  import { resolveSpawnCommand } from "../spawn.js";
13
14
  import { discoverIntegrationSpecFiles as discoverIntegrationSpecFilesShared } from "./integration-discovery.js";
@@ -40,6 +41,17 @@ function resolveEphemeralPostgresImage(env2 = process.env) {
40
41
  function ephemeralPostgresInitSql() {
41
42
  return EPHEMERAL_POSTGRES_INIT_SQL;
42
43
  }
44
+ const DEFAULT_EPHEMERAL_JWT_SECRET = "om-ephemeral-integration-jwt-secret";
45
+ function resolveEphemeralJwtSecret(env2 = process.env) {
46
+ const inherited = env2.JWT_SECRET;
47
+ if (inherited && !isUnsafeJwtSecret(inherited)) return inherited;
48
+ if (inherited !== void 0) {
49
+ console.warn(
50
+ "[integration] Ignoring the inherited JWT_SECRET: it is a placeholder or too short and the ephemeral app runs with NODE_ENV=production. Using the ephemeral default instead."
51
+ );
52
+ }
53
+ return DEFAULT_EPHEMERAL_JWT_SECRET;
54
+ }
43
55
  const PLAYWRIGHT_ENV_UNAVAILABLE_PATTERNS = [
44
56
  /net::ERR_CONNECTION_REFUSED/i,
45
57
  /Failed to connect to .* (localhost|127\.0\.0\.1)/i,
@@ -1567,8 +1579,15 @@ function buildReusableEnvironment(baseUrl, databaseUrl, queueBaseDir, captureScr
1567
1579
  // stale CRUD response until the TTL (TC-CRM-028/079, TC-SX-001).
1568
1580
  CACHE_STRATEGY: "sqlite",
1569
1581
  CACHE_SQLITE_PATH: EPHEMERAL_CACHE_DB_PATH,
1570
- JWT_SECRET: process.env.JWT_SECRET ?? "om-ephemeral-integration-jwt-secret",
1582
+ JWT_SECRET: resolveEphemeralJwtSecret(),
1571
1583
  OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? "om-ephemeral-integration-mfa-setup-secret",
1584
+ // The forms module refuses its DEV-ONLY deterministic KMS adapter under
1585
+ // NODE_ENV=production (which is how this app runs), so without a master key
1586
+ // `formsEncryptionService` fails to resolve and every encrypted-payload
1587
+ // route answers a bare 500: the whole public/portal runtime, the submissions
1588
+ // inbox, revisions, audit, anonymize, export, PDF and analytics. Admin CRUD
1589
+ // keeps working, which is what makes the omission easy to miss.
1590
+ FORMS_ENCRYPTION_MASTER_KEY: process.env.FORMS_ENCRYPTION_MASTER_KEY ?? "6f6d2d657068656d6572616c2d696e746567726174696f6e2d666f726d732d31",
1572
1591
  // Integration probe + tests expect `admin@acme.com / secret` and
1573
1592
  // `employee@acme.com / secret`. NODE_ENV=production routes derived-user
1574
1593
  // password resolution through the random-fallback branch unless these
@@ -2771,8 +2790,15 @@ async function startEphemeralEnvironment(options) {
2771
2790
  APP_URL: applicationBaseUrl,
2772
2791
  NEXT_PUBLIC_APP_URL: applicationBaseUrl,
2773
2792
  PLATFORM_PORTAL_BASE_URL: applicationBaseUrl,
2774
- JWT_SECRET: process.env.JWT_SECRET ?? "om-ephemeral-integration-jwt-secret",
2793
+ JWT_SECRET: resolveEphemeralJwtSecret(),
2775
2794
  OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? "om-ephemeral-integration-mfa-setup-secret",
2795
+ // The forms module refuses its DEV-ONLY deterministic KMS adapter under
2796
+ // NODE_ENV=production (which is how this app runs), so without a master key
2797
+ // `formsEncryptionService` fails to resolve and every encrypted-payload
2798
+ // route answers a bare 500: the whole public/portal runtime, the submissions
2799
+ // inbox, revisions, audit, anonymize, export, PDF and analytics. Admin CRUD
2800
+ // keeps working, which is what makes the omission easy to miss.
2801
+ FORMS_ENCRYPTION_MASTER_KEY: process.env.FORMS_ENCRYPTION_MASTER_KEY ?? "6f6d2d657068656d6572616c2d696e746567726174696f6e2d666f726d732d31",
2776
2802
  NODE_ENV: "production",
2777
2803
  // See the auth-probe block above: pin derived-user passwords to the
2778
2804
  // documented 'secret' so the ephemeral login probe converges under
@@ -3268,6 +3294,7 @@ export {
3268
3294
  registerEphemeralShutdownHandlers,
3269
3295
  resolveAppReadyTimeoutMs,
3270
3296
  resolveBuildCacheTtlSeconds,
3297
+ resolveEphemeralJwtSecret,
3271
3298
  resolveEphemeralPostgresImage,
3272
3299
  runEphemeralAppForQa,
3273
3300
  runIntegrationCoverageReport,