@powerhousedao/shared 6.0.0-dev.238 → 6.0.0-dev.239

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,11 @@
1
1
  //#region clis/services/telemetry.d.ts
2
+ type TelemetryClient = {
3
+ /**
4
+ * Captures an error (if telemetry is initialized) and flushes before the
5
+ * caller calls process.exit(). Safe no-op when telemetry is disabled.
6
+ */
7
+ captureCliError: (err: unknown) => Promise<void>;
8
+ };
2
9
  /**
3
10
  * Prompts the user once, caches the answer. Must be called before init.
4
11
  * Returns `true` if telemetry should be enabled, `false` otherwise.
@@ -11,12 +18,7 @@ declare function resolveTelemetryConsent(): Promise<boolean>;
11
18
  declare function initCliTelemetry(opts: {
12
19
  cliName: "ph-cli" | "ph-cmd";
13
20
  release?: string;
14
- }): Promise<void>;
15
- /**
16
- * Captures an error (if telemetry is initialized) and flushes before the
17
- * caller calls process.exit(). Safe no-op when telemetry is disabled.
18
- */
19
- declare function captureCliError(err: unknown): Promise<void>;
21
+ }): Promise<TelemetryClient | undefined>;
20
22
  /**
21
23
  * Explicitly set telemetry consent (used by `ph telemetry on|off`).
22
24
  */
@@ -36,5 +38,5 @@ declare function getTelemetryStatus(): {
36
38
  enabled: false;
37
39
  };
38
40
  //#endregion
39
- export { captureCliError, getTelemetryStatus, initCliTelemetry, resolveTelemetryConsent, setTelemetryConsent };
41
+ export { TelemetryClient, getTelemetryStatus, initCliTelemetry, resolveTelemetryConsent, setTelemetryConsent };
40
42
  //# sourceMappingURL=telemetry.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.d.mts","names":[],"sources":["../../../clis/services/telemetry.ts"],"mappings":";;AA8EA;;;iBAAsB,uBAAA,CAAA,GAA2B,OAAA;;AAkHjD;;;iBAAsB,gBAAA,CAAiB,IAAA;EACrC,OAAA;EACA,OAAA;AAAA,IACE,OAAA;;;;AAkCJ;iBAAsB,eAAA,CAAgB,GAAA,YAAe,OAAA;;;;iBAcrC,mBAAA,CAAoB,OAAA;;;;iBAOpB,kBAAA,CAAA;EACV,MAAA;EAAe,OAAA;AAAA;EACf,MAAA;EAAkB,OAAA;EAAkB,OAAA;AAAA;EACpC,MAAA;EAAmB,OAAA;AAAA"}
1
+ {"version":3,"file":"telemetry.d.mts","names":[],"sources":["../../../clis/services/telemetry.ts"],"mappings":";KAmCY,eAAA;EAAA;;;;EAKV,eAAA,GAAkB,GAAA,cAAiB,OAAA;AAAA;;;;AA8CrC;iBAAsB,uBAAA,CAAA,GAA2B,OAAA;;;;AAgHjD;iBAAsB,gBAAA,CAAiB,IAAA;EACrC,OAAA;EACA,OAAA;AAAA,IACE,OAAA,CAAQ,eAAA;;;;iBA4CI,mBAAA,CAAoB,OAAA;;;AAApC;iBAOgB,kBAAA,CAAA;EACV,MAAA;EAAe,OAAA;AAAA;EACf,MAAA;EAAkB,OAAA;EAAkB,OAAA;AAAA;EACpC,MAAA;EAAmB,OAAA;AAAA"}
@@ -107,16 +107,14 @@ function scrubEvent(event) {
107
107
  } catch {}
108
108
  return event;
109
109
  }
110
- let initialized = false;
111
110
  /**
112
111
  * Initializes Sentry for CLI error reporting if telemetry is enabled.
113
112
  * Safe to call multiple times; only the first call takes effect.
114
113
  */
115
114
  async function initCliTelemetry(opts) {
116
- if (initialized) return;
117
115
  if (!await resolveTelemetryConsent()) return;
118
- const Sentry = await import("@sentry/node");
119
- Sentry.init({
116
+ const Sentry = await import("@sentry/node-core/light");
117
+ const sentryClient = Sentry.init({
120
118
  dsn: SENTRY_DSN,
121
119
  release: opts.release,
122
120
  environment: process.env.NODE_ENV || "production",
@@ -133,19 +131,13 @@ async function initCliTelemetry(opts) {
133
131
  });
134
132
  Sentry.setTag("cli_name", opts.cliName);
135
133
  if (opts.release) Sentry.setTag("cli_version", opts.release);
136
- initialized = true;
137
- }
138
- /**
139
- * Captures an error (if telemetry is initialized) and flushes before the
140
- * caller calls process.exit(). Safe no-op when telemetry is disabled.
141
- */
142
- async function captureCliError(err) {
143
- if (!initialized) return;
144
- try {
145
- const Sentry = await import("@sentry/node");
146
- Sentry.captureException(err);
147
- await Sentry.flush(2e3);
148
- } catch {}
134
+ if (!sentryClient) return;
135
+ return { captureCliError: async (err) => {
136
+ try {
137
+ sentryClient.captureException(err);
138
+ await sentryClient.flush(2e3);
139
+ } catch {}
140
+ } };
149
141
  }
150
142
  /**
151
143
  * Explicitly set telemetry consent (used by `ph telemetry on|off`).
@@ -180,6 +172,6 @@ function getTelemetryStatus() {
180
172
  };
181
173
  }
182
174
  //#endregion
183
- export { captureCliError, getTelemetryStatus, initCliTelemetry, resolveTelemetryConsent, setTelemetryConsent };
175
+ export { getTelemetryStatus, initCliTelemetry, resolveTelemetryConsent, setTelemetryConsent };
184
176
 
185
177
  //# sourceMappingURL=telemetry.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.mjs","names":[],"sources":["../../../clis/services/telemetry.ts"],"sourcesContent":["/**\n * CLI telemetry (error reporting via Sentry).\n *\n * Design:\n * - Opt-out by default, asked once on first interactive run.\n * - Stores consent in ~/.ph/telemetry.json so we never ask twice.\n * - Respects PH_NO_TELEMETRY=1 and DO_NOT_TRACK=1 as immediate kill switches.\n * - Non-interactive (TTY missing, CI, piped) defaults to DISABLED — we don't\n * want to hang a CI pipeline on an unanswered prompt, and we don't want to\n * capture errors without informed consent.\n * - DSN is published in the CLI binary; Sentry DSNs accept events but grant\n * no read access, so this is safe. Hardcoded so users can't accidentally\n * misroute events.\n *\n * PII scrubbing in beforeSend hook:\n * - Home-directory paths collapsed to ~\n * - Flag/arg values that look like secrets (tokens, keys) stripped\n * - No source-context from user files\n */\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// Sentry project \"ph-cli\" on the powerhouse-hosted Sentry instance.\n// Public DSNs are safe to ship — they grant write-only ingest access.\nconst SENTRY_DSN =\n \"https://0e7793802288589b4923896118374462@sentry.monitoring.vetra.io/3\";\n\nconst TELEMETRY_FILE = join(homedir(), \".ph\", \"telemetry.json\");\n\ntype TelemetryConfig = {\n enabled: boolean;\n askedAt: string;\n};\n\nfunction isExplicitlyDisabled(): boolean {\n // Standard opt-out signals respected by most OSS CLIs.\n return (\n process.env.PH_NO_TELEMETRY === \"1\" ||\n process.env.PH_NO_TELEMETRY === \"true\" ||\n process.env.DO_NOT_TRACK === \"1\" ||\n process.env.DO_NOT_TRACK === \"true\"\n );\n}\n\nfunction isExplicitlyEnabled(): boolean {\n return (\n process.env.PH_TELEMETRY === \"1\" || process.env.PH_TELEMETRY === \"true\"\n );\n}\n\nfunction isInteractive(): boolean {\n // Only ask if stdin is a TTY and CI env isn't set.\n return Boolean(process.stdin.isTTY) && !process.env.CI;\n}\n\nfunction readConfig(): TelemetryConfig | null {\n try {\n if (!existsSync(TELEMETRY_FILE)) return null;\n return JSON.parse(readFileSync(TELEMETRY_FILE, \"utf8\")) as TelemetryConfig;\n } catch {\n return null;\n }\n}\n\nfunction writeConfig(cfg: TelemetryConfig): void {\n try {\n mkdirSync(join(homedir(), \".ph\"), { recursive: true });\n writeFileSync(TELEMETRY_FILE, JSON.stringify(cfg, null, 2));\n } catch {\n // non-fatal; we'll just ask again next time\n }\n}\n\n/**\n * Prompts the user once, caches the answer. Must be called before init.\n * Returns `true` if telemetry should be enabled, `false` otherwise.\n */\nexport async function resolveTelemetryConsent(): Promise<boolean> {\n if (isExplicitlyDisabled()) return false;\n if (isExplicitlyEnabled()) return true;\n\n const cached = readConfig();\n if (cached) return cached.enabled;\n\n if (!isInteractive()) {\n // Non-interactive first run: stay silent, don't ask, don't send. User can\n // opt in later with `ph telemetry on` or PH_TELEMETRY=1.\n return false;\n }\n\n const enquirer = await import(\"enquirer\");\n try {\n const { enabled } = await enquirer.default.prompt<{ enabled: boolean }>({\n type: \"confirm\",\n name: \"enabled\",\n message:\n \"Help improve Powerhouse by sending anonymous error reports? \" +\n \"(stack traces only, paths and secrets are scrubbed)\",\n initial: true,\n });\n writeConfig({ enabled, askedAt: new Date().toISOString() });\n return enabled;\n } catch {\n // user hit Ctrl-C during prompt — treat as no, but don't persist so we\n // ask again next time\n return false;\n }\n}\n\nfunction scrubString(input: string): string {\n if (!input) return input;\n const home = homedir();\n let out = input;\n // Collapse home dir to ~\n if (home && out.includes(home)) {\n out = out.split(home).join(\"~\");\n }\n // Strip common secret-shaped flag values: --token=XYZ, --api-key XYZ, etc.\n out = out.replace(\n /(--?(?:token|api[-_]?key|password|secret|auth)[=\\s])([^\\s]+)/gi,\n \"$1<redacted>\",\n );\n return out;\n}\n\ntype ScrubbableFrame = {\n filename?: string;\n abs_path?: string;\n pre_context?: unknown;\n context_line?: unknown;\n post_context?: unknown;\n vars?: unknown;\n};\n\ntype ScrubbableException = {\n value?: string;\n stacktrace?: { frames?: ScrubbableFrame[] };\n};\n\ntype ScrubbableEvent = {\n message?: string;\n logentry?: { message?: string };\n exception?: { values?: ScrubbableException[] };\n server_name?: string;\n extra?: Record<string, unknown>;\n};\n\nfunction scrubEvent<T>(event: T): T {\n const e = event as ScrubbableEvent;\n try {\n if (e.message) e.message = scrubString(e.message);\n if (e.logentry?.message) {\n e.logentry.message = scrubString(e.logentry.message);\n }\n const values = e.exception?.values;\n if (Array.isArray(values)) {\n for (const ex of values) {\n if (ex.value) ex.value = scrubString(ex.value);\n const frames = ex.stacktrace?.frames;\n if (Array.isArray(frames)) {\n for (const f of frames) {\n if (f.filename) f.filename = scrubString(f.filename);\n if (f.abs_path) f.abs_path = scrubString(f.abs_path);\n // Drop captured source context from user machines — privacy.\n delete f.pre_context;\n delete f.context_line;\n delete f.post_context;\n delete f.vars;\n }\n }\n }\n }\n // Server name can leak the user's machine hostname; drop it.\n delete e.server_name;\n // Strip raw argv from extra context.\n if (e.extra) {\n delete e.extra.argv;\n delete e.extra.env;\n }\n } catch {\n // Never let scrubbing throw — worst case we drop the event below.\n }\n return event;\n}\n\nlet initialized = false;\n\n/**\n * Initializes Sentry for CLI error reporting if telemetry is enabled.\n * Safe to call multiple times; only the first call takes effect.\n */\nexport async function initCliTelemetry(opts: {\n cliName: \"ph-cli\" | \"ph-cmd\";\n release?: string;\n}): Promise<void> {\n if (initialized) return;\n const enabled = await resolveTelemetryConsent();\n if (!enabled) return;\n\n const Sentry = await import(\"@sentry/node\");\n Sentry.init({\n dsn: SENTRY_DSN,\n release: opts.release,\n environment: process.env.NODE_ENV || \"production\",\n sendDefaultPii: false,\n defaultIntegrations: false,\n integrations: [\n // Only the bare minimum — no automatic HTTP/FS instrumentation.\n ],\n beforeSend(event) {\n return scrubEvent(event);\n },\n beforeBreadcrumb(breadcrumb) {\n if (breadcrumb.message) {\n breadcrumb.message = scrubString(breadcrumb.message);\n }\n return breadcrumb;\n },\n });\n Sentry.setTag(\"cli_name\", opts.cliName);\n if (opts.release) Sentry.setTag(\"cli_version\", opts.release);\n initialized = true;\n}\n\n/**\n * Captures an error (if telemetry is initialized) and flushes before the\n * caller calls process.exit(). Safe no-op when telemetry is disabled.\n */\nexport async function captureCliError(err: unknown): Promise<void> {\n if (!initialized) return;\n try {\n const Sentry = await import(\"@sentry/node\");\n Sentry.captureException(err);\n await Sentry.flush(2000);\n } catch {\n // Reporting must never mask the real error.\n }\n}\n\n/**\n * Explicitly set telemetry consent (used by `ph telemetry on|off`).\n */\nexport function setTelemetryConsent(enabled: boolean): void {\n writeConfig({ enabled, askedAt: new Date().toISOString() });\n}\n\n/**\n * Returns the current telemetry state — useful for `ph telemetry status`.\n */\nexport function getTelemetryStatus():\n | { source: \"env\"; enabled: boolean }\n | { source: \"config\"; enabled: boolean; askedAt: string }\n | { source: \"default\"; enabled: false } {\n if (isExplicitlyDisabled()) return { source: \"env\", enabled: false };\n if (isExplicitlyEnabled()) return { source: \"env\", enabled: true };\n const cached = readConfig();\n if (cached)\n return {\n source: \"config\",\n enabled: cached.enabled,\n askedAt: cached.askedAt,\n };\n return { source: \"default\", enabled: false };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"telemetry.mjs","names":[],"sources":["../../../clis/services/telemetry.ts"],"sourcesContent":["/**\n * CLI telemetry (error reporting via Sentry).\n *\n * Design:\n * - Opt-out by default, asked once on first interactive run.\n * - Stores consent in ~/.ph/telemetry.json so we never ask twice.\n * - Respects PH_NO_TELEMETRY=1 and DO_NOT_TRACK=1 as immediate kill switches.\n * - Non-interactive (TTY missing, CI, piped) defaults to DISABLED — we don't\n * want to hang a CI pipeline on an unanswered prompt, and we don't want to\n * capture errors without informed consent.\n * - DSN is published in the CLI binary; Sentry DSNs accept events but grant\n * no read access, so this is safe. Hardcoded so users can't accidentally\n * misroute events.\n *\n * PII scrubbing in beforeSend hook:\n * - Home-directory paths collapsed to ~\n * - Flag/arg values that look like secrets (tokens, keys) stripped\n * - No source-context from user files\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// Sentry project \"ph-cli\" on the powerhouse-hosted Sentry instance.\n// Public DSNs are safe to ship — they grant write-only ingest access.\nconst SENTRY_DSN =\n \"https://0e7793802288589b4923896118374462@sentry.monitoring.vetra.io/3\";\n\nconst TELEMETRY_FILE = join(homedir(), \".ph\", \"telemetry.json\");\n\ntype TelemetryConfig = {\n enabled: boolean;\n askedAt: string;\n};\n\nexport type TelemetryClient = {\n /**\n * Captures an error (if telemetry is initialized) and flushes before the\n * caller calls process.exit(). Safe no-op when telemetry is disabled.\n */\n captureCliError: (err: unknown) => Promise<void>;\n};\n\nfunction isExplicitlyDisabled(): boolean {\n // Standard opt-out signals respected by most OSS CLIs.\n return (\n process.env.PH_NO_TELEMETRY === \"1\" ||\n process.env.PH_NO_TELEMETRY === \"true\" ||\n process.env.DO_NOT_TRACK === \"1\" ||\n process.env.DO_NOT_TRACK === \"true\"\n );\n}\n\nfunction isExplicitlyEnabled(): boolean {\n return (\n process.env.PH_TELEMETRY === \"1\" || process.env.PH_TELEMETRY === \"true\"\n );\n}\n\nfunction isInteractive(): boolean {\n // Only ask if stdin is a TTY and CI env isn't set.\n return Boolean(process.stdin.isTTY) && !process.env.CI;\n}\n\nfunction readConfig(): TelemetryConfig | null {\n try {\n if (!existsSync(TELEMETRY_FILE)) return null;\n return JSON.parse(readFileSync(TELEMETRY_FILE, \"utf8\")) as TelemetryConfig;\n } catch {\n return null;\n }\n}\n\nfunction writeConfig(cfg: TelemetryConfig): void {\n try {\n mkdirSync(join(homedir(), \".ph\"), { recursive: true });\n writeFileSync(TELEMETRY_FILE, JSON.stringify(cfg, null, 2));\n } catch {\n // non-fatal; we'll just ask again next time\n }\n}\n\n/**\n * Prompts the user once, caches the answer. Must be called before init.\n * Returns `true` if telemetry should be enabled, `false` otherwise.\n */\nexport async function resolveTelemetryConsent(): Promise<boolean> {\n if (isExplicitlyDisabled()) return false;\n if (isExplicitlyEnabled()) return true;\n\n const cached = readConfig();\n if (cached) return cached.enabled;\n\n if (!isInteractive()) {\n // Non-interactive first run: stay silent, don't ask, don't send. User can\n // opt in later with `ph telemetry on` or PH_TELEMETRY=1.\n return false;\n }\n\n const enquirer = await import(\"enquirer\");\n try {\n const { enabled } = await enquirer.default.prompt<{ enabled: boolean }>({\n type: \"confirm\",\n name: \"enabled\",\n message:\n \"Help improve Powerhouse by sending anonymous error reports? \" +\n \"(stack traces only, paths and secrets are scrubbed)\",\n initial: true,\n });\n writeConfig({ enabled, askedAt: new Date().toISOString() });\n return enabled;\n } catch {\n // user hit Ctrl-C during prompt — treat as no, but don't persist so we\n // ask again next time\n return false;\n }\n}\n\nfunction scrubString(input: string): string {\n if (!input) return input;\n const home = homedir();\n let out = input;\n // Collapse home dir to ~\n if (home && out.includes(home)) {\n out = out.split(home).join(\"~\");\n }\n // Strip common secret-shaped flag values: --token=XYZ, --api-key XYZ, etc.\n out = out.replace(\n /(--?(?:token|api[-_]?key|password|secret|auth)[=\\s])([^\\s]+)/gi,\n \"$1<redacted>\",\n );\n return out;\n}\n\ntype ScrubbableFrame = {\n filename?: string;\n abs_path?: string;\n pre_context?: unknown;\n context_line?: unknown;\n post_context?: unknown;\n vars?: unknown;\n};\n\ntype ScrubbableException = {\n value?: string;\n stacktrace?: { frames?: ScrubbableFrame[] };\n};\n\ntype ScrubbableEvent = {\n message?: string;\n logentry?: { message?: string };\n exception?: { values?: ScrubbableException[] };\n server_name?: string;\n extra?: Record<string, unknown>;\n};\n\nfunction scrubEvent<T>(event: T): T {\n const e = event as ScrubbableEvent;\n try {\n if (e.message) e.message = scrubString(e.message);\n if (e.logentry?.message) {\n e.logentry.message = scrubString(e.logentry.message);\n }\n const values = e.exception?.values;\n if (Array.isArray(values)) {\n for (const ex of values) {\n if (ex.value) ex.value = scrubString(ex.value);\n const frames = ex.stacktrace?.frames;\n if (Array.isArray(frames)) {\n for (const f of frames) {\n if (f.filename) f.filename = scrubString(f.filename);\n if (f.abs_path) f.abs_path = scrubString(f.abs_path);\n // Drop captured source context from user machines — privacy.\n delete f.pre_context;\n delete f.context_line;\n delete f.post_context;\n delete f.vars;\n }\n }\n }\n }\n // Server name can leak the user's machine hostname; drop it.\n delete e.server_name;\n // Strip raw argv from extra context.\n if (e.extra) {\n delete e.extra.argv;\n delete e.extra.env;\n }\n } catch {\n // Never let scrubbing throw — worst case we drop the event below.\n }\n return event;\n}\n\n/**\n * Initializes Sentry for CLI error reporting if telemetry is enabled.\n * Safe to call multiple times; only the first call takes effect.\n */\nexport async function initCliTelemetry(opts: {\n cliName: \"ph-cli\" | \"ph-cmd\";\n release?: string;\n}): Promise<TelemetryClient | undefined> {\n const enabled = await resolveTelemetryConsent();\n if (!enabled) return;\n\n const Sentry = await import(\"@sentry/node-core/light\");\n const sentryClient = Sentry.init({\n dsn: SENTRY_DSN,\n release: opts.release,\n environment: process.env.NODE_ENV || \"production\",\n sendDefaultPii: false,\n defaultIntegrations: false,\n integrations: [\n // Only the bare minimum — no automatic HTTP/FS instrumentation.\n ],\n beforeSend(event) {\n return scrubEvent(event);\n },\n beforeBreadcrumb(breadcrumb) {\n if (breadcrumb.message) {\n breadcrumb.message = scrubString(breadcrumb.message);\n }\n return breadcrumb;\n },\n });\n Sentry.setTag(\"cli_name\", opts.cliName);\n if (opts.release) Sentry.setTag(\"cli_version\", opts.release);\n if (!sentryClient) {\n return;\n }\n return {\n captureCliError: async (err: unknown) => {\n try {\n sentryClient.captureException(err);\n await sentryClient.flush(2000);\n } catch {\n // Reporting must never mask the real error.\n }\n },\n };\n}\n\n/**\n * Explicitly set telemetry consent (used by `ph telemetry on|off`).\n */\nexport function setTelemetryConsent(enabled: boolean): void {\n writeConfig({ enabled, askedAt: new Date().toISOString() });\n}\n\n/**\n * Returns the current telemetry state — useful for `ph telemetry status`.\n */\nexport function getTelemetryStatus():\n | { source: \"env\"; enabled: boolean }\n | { source: \"config\"; enabled: boolean; askedAt: string }\n | { source: \"default\"; enabled: false } {\n if (isExplicitlyDisabled()) return { source: \"env\", enabled: false };\n if (isExplicitlyEnabled()) return { source: \"env\", enabled: true };\n const cached = readConfig();\n if (cached)\n return {\n source: \"config\",\n enabled: cached.enabled,\n askedAt: cached.askedAt,\n };\n return { source: \"default\", enabled: false };\n}\n"],"mappings":"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"}
@@ -23,10 +23,10 @@ declare const buildEnvSchema: z.ZodObject<{
23
23
  declare const runtimeEnvSchema: z.ZodObject<{
24
24
  PH_CONNECT_VERSION: z.ZodDefault<z.ZodString>;
25
25
  PH_CONNECT_LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
26
- error: "error";
27
26
  debug: "debug";
28
27
  info: "info";
29
28
  warn: "warn";
29
+ error: "error";
30
30
  }>>;
31
31
  PH_CONNECT_REQUIRES_HARD_REFRESH: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodBoolean, z.ZodEnum<{
32
32
  true: "true";
@@ -168,10 +168,10 @@ declare const connectEnvSchema: z.ZodObject<{
168
168
  PH_SENTRY_PROJECT: z.ZodOptional<z.ZodString>;
169
169
  PH_CONNECT_VERSION: z.ZodDefault<z.ZodString>;
170
170
  PH_CONNECT_LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
171
- error: "error";
172
171
  debug: "debug";
173
172
  info: "info";
174
173
  warn: "warn";
174
+ error: "error";
175
175
  }>>;
176
176
  PH_CONNECT_REQUIRES_HARD_REFRESH: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodBoolean, z.ZodEnum<{
177
177
  true: "true";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/shared",
3
- "version": "6.0.0-dev.238",
3
+ "version": "6.0.0-dev.239",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -97,10 +97,14 @@
97
97
  "./clis/telemetry": {
98
98
  "source": "./clis/services/telemetry.ts",
99
99
  "import": "./dist/clis/services/telemetry.mjs"
100
+ },
101
+ "./build-config": {
102
+ "source": "./clis/build-config.mts",
103
+ "import": "./dist/clis/build-config.mjs"
100
104
  }
101
105
  },
102
106
  "dependencies": {
103
- "@sentry/node": "^9.47.1",
107
+ "@sentry/node-core": "^10.52.0",
104
108
  "@sindresorhus/fnv1a": "3.1.0",
105
109
  "chalk": "5.6.2",
106
110
  "change-case": "5.4.4",
@@ -126,7 +130,7 @@
126
130
  "@types/luxon": "3.7.1",
127
131
  "@types/npm-package-arg": "6.1.4",
128
132
  "@types/react": "19.2.14",
129
- "react": "19.2.4",
133
+ "react": "19.2.6",
130
134
  "tsdown": "0.21.1",
131
135
  "tsx": "4.21.0"
132
136
  },