@opengeni/config 0.7.22 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/config",
3
- "version": "0.7.22",
3
+ "version": "0.9.1",
4
4
  "description": "OpenGeni runtime configuration: settings resolution, deployment knobs, and config validation shared across the server packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@opengeni/codex": "^0.2.9",
37
- "@opengeni/contracts": "^0.26.1",
37
+ "@opengeni/contracts": "^0.28.1",
38
38
  "zod": "^4.2.1"
39
39
  },
40
40
  "engines": {
package/src/index.ts CHANGED
@@ -218,6 +218,20 @@ const SettingsSchema = z.object({
218
218
  observabilityMetricsEnabled: EnvBoolean.default(true),
219
219
  observabilityOtlpEndpoint: z.string().url().optional(),
220
220
  observabilityOtlpHeaders: z.string().default(""),
221
+ analyticsEnabled: EnvBoolean.default(false),
222
+ analyticsConsentRequired: EnvBoolean.default(true),
223
+ analyticsReoClientId: z
224
+ .string()
225
+ .max(128)
226
+ .regex(/^[A-Za-z0-9_-]+$/u)
227
+ .optional(),
228
+ analyticsPosthogProjectKey: z.string().min(1).max(256).optional(),
229
+ analyticsPosthogHost: z.string().url().max(2_048).optional(),
230
+ analyticsGa4MeasurementId: z
231
+ .string()
232
+ .max(32)
233
+ .regex(/^G-[A-Z0-9]+$/u)
234
+ .optional(),
221
235
  publicBaseUrl: z.string().url().optional(),
222
236
  // Browser origin when the web app and API use separate origins in local
223
237
  // development. Production normally leaves this unset and uses publicBaseUrl.
@@ -269,11 +283,15 @@ const SettingsSchema = z.object({
269
283
  integrationsOauthClientsJson: z.string().default("{}"),
270
284
  slackClientId: z.string().optional(),
271
285
  slackClientSecret: z.string().optional(),
286
+ slackSigningSecret: z.string().optional(),
272
287
  googleDriveClientId: z.string().optional(),
273
288
  googleDriveClientSecret: z.string().optional(),
274
289
  // Undefined is meaningful: the migration boundary persists the product
275
290
  // default of 3 when no deployment override is supplied.
276
291
  maxNestedAgentDepth: z.coerce.number().int().nonnegative().max(MAX_NESTED_AGENT_DEPTH).optional(),
292
+ // Operator OAuth apps for first-party social connectors, keyed by provider
293
+ // id ("x", "reddit"): {"x":{"clientId":"...","clientSecret":"..."}}.
294
+ socialOauthClientsJson: z.string().default("{}"),
277
295
  // Session goal guard rails. Goals are designed for runs that legitimately
278
296
  // span days, so length is bounded by pathology detection (no-progress
279
297
  // streaks, budget exhaustion), never by count. goalMaxAutoContinuations is
@@ -328,6 +346,9 @@ const SettingsSchema = z.object({
328
346
  apiPort: z.coerce.number().int().positive().default(8000),
329
347
  workerHttpPort: z.coerce.number().int().positive().default(8001),
330
348
  opengeniMcpUrl: z.string().url().optional(),
349
+ // Origins allowed to send browser cookies cross-origin. Other origins may
350
+ // call the public API with bearer credentials, but never receive credentialed
351
+ // CORS responses.
331
352
  corsAllowOriginRegex: z.string().default(String.raw`^https?://(localhost|127\.0\.0\.1)(:\d+)?$`),
332
353
  openaiProvider: z.enum(["openai", "azure"]).default("openai"),
333
354
  openaiApiKey: z.string().optional(),
@@ -1016,7 +1037,11 @@ export function resolveVoiceInputProviderRegistry(settings: Settings): VoiceInpu
1016
1037
  // subscription turns. VOICE_INPUT_CODEX_EXPERIMENTAL is retained for
1017
1038
  // back-compat docs/env but no longer gates inclusion.
1018
1039
  if (!settings.codexSubscriptionEnabled) continue;
1019
- providers.push({ id: "codex-subscription", kind: "codex-subscription", experimental: true });
1040
+ providers.push({
1041
+ id: "codex-subscription",
1042
+ kind: "codex-subscription",
1043
+ experimental: true,
1044
+ });
1020
1045
  }
1021
1046
  }
1022
1047
  return providers;
@@ -1563,6 +1588,12 @@ export function getSettings(): Settings {
1563
1588
  optional("OPENGENI_OTEL_EXPORTER_OTLP_ENDPOINT") ?? optional("OTEL_EXPORTER_OTLP_ENDPOINT"),
1564
1589
  observabilityOtlpHeaders:
1565
1590
  optional("OPENGENI_OTEL_EXPORTER_OTLP_HEADERS") ?? optional("OTEL_EXPORTER_OTLP_HEADERS"),
1591
+ analyticsEnabled: optional("OPENGENI_ANALYTICS_ENABLED"),
1592
+ analyticsConsentRequired: optional("OPENGENI_ANALYTICS_CONSENT_REQUIRED"),
1593
+ analyticsReoClientId: optional("OPENGENI_ANALYTICS_REO_CLIENT_ID"),
1594
+ analyticsPosthogProjectKey: optional("OPENGENI_ANALYTICS_POSTHOG_PROJECT_KEY"),
1595
+ analyticsPosthogHost: optional("OPENGENI_ANALYTICS_POSTHOG_HOST"),
1596
+ analyticsGa4MeasurementId: optional("OPENGENI_ANALYTICS_GA4_MEASUREMENT_ID"),
1566
1597
  publicBaseUrl: optional("OPENGENI_PUBLIC_BASE_URL"),
1567
1598
  webBaseUrl: optional("OPENGENI_WEB_BASE_URL"),
1568
1599
  agentReleasesBaseUrl: optional("OPENGENI_AGENT_RELEASES_BASE_URL"),
@@ -1588,9 +1619,11 @@ export function getSettings(): Settings {
1588
1619
  integrationsOauthClientsJson: optional("OPENGENI_INTEGRATIONS_OAUTH_CLIENTS_JSON"),
1589
1620
  slackClientId: optional("OPENGENI_SLACK_CLIENT_ID"),
1590
1621
  slackClientSecret: optional("OPENGENI_SLACK_CLIENT_SECRET"),
1622
+ slackSigningSecret: optional("OPENGENI_SLACK_SIGNING_SECRET"),
1591
1623
  googleDriveClientId: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_ID"),
1592
1624
  googleDriveClientSecret: optional("OPENGENI_GOOGLE_DRIVE_CLIENT_SECRET"),
1593
1625
  maxNestedAgentDepth: optional("OPENGENI_MAX_NESTED_AGENT_DEPTH"),
1626
+ socialOauthClientsJson: optional("OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON"),
1594
1627
  goalMaxAutoContinuations: optional("OPENGENI_GOAL_MAX_AUTO_CONTINUATIONS"),
1595
1628
  goalNoProgressLimit: optional("OPENGENI_GOAL_NO_PROGRESS_LIMIT"),
1596
1629
  agentMaxModelCallsPerTurn: optional("OPENGENI_AGENT_MAX_MODEL_CALLS_PER_TURN"),
@@ -1820,6 +1853,23 @@ export function getSettings(): Settings {
1820
1853
  return settings;
1821
1854
  }
1822
1855
 
1856
+ const LOCAL_FIRST_PARTY_DELEGATION_SECRET = "opengeni-local-first-party-delegation-secret-v1";
1857
+
1858
+ /**
1859
+ * First-party session tools need a shared HMAC identity even in the unauthenticated
1860
+ * local product mode. A fixed local-only value is no broader than that mode's
1861
+ * existing access boundary, while configured and managed deployments continue to
1862
+ * require an operator-provided secret.
1863
+ */
1864
+ export function resolveFirstPartyDelegationSecret(settings: Settings): string | undefined {
1865
+ const explicit = settings.delegationSecret?.trim();
1866
+ if (explicit) return explicit;
1867
+ return settings.productAccessMode === "local" &&
1868
+ (settings.environment === "local" || settings.environment === "test")
1869
+ ? LOCAL_FIRST_PARTY_DELEGATION_SECRET
1870
+ : undefined;
1871
+ }
1872
+
1823
1873
  /**
1824
1874
  * The Modal sandbox idle timeout (seconds) the provider actually passes as
1825
1875
  * idleTimeoutMs (sandbox-file-persistence). When the operator did not pin
@@ -2271,8 +2321,16 @@ function builtinCredentialSource(settings: Settings): CredentialSourceV1 {
2271
2321
  }
2272
2322
 
2273
2323
  function staticRequestMetadataForDigest(provider: ResolvedModelProvider): {
2274
- headers: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
2275
- query: Array<{ name: string; classification: "public" | "secret"; value?: string }>;
2324
+ headers: Array<{
2325
+ name: string;
2326
+ classification: "public" | "secret";
2327
+ value?: string;
2328
+ }>;
2329
+ query: Array<{
2330
+ name: string;
2331
+ classification: "public" | "secret";
2332
+ value?: string;
2333
+ }>;
2276
2334
  } {
2277
2335
  const publicHeaders = new Set(provider.publicDefaultHeaderNames ?? []);
2278
2336
  const publicQuery = new Set(provider.publicDefaultQueryNames ?? []);
@@ -2450,7 +2508,10 @@ export function withCodexCatalogProvider(settings: Settings): Settings {
2450
2508
  };
2451
2509
  }),
2452
2510
  };
2453
- return { ...settings, modelProvidersJson: JSON.stringify([...providers, provider]) };
2511
+ return {
2512
+ ...settings,
2513
+ modelProvidersJson: JSON.stringify([...providers, provider]),
2514
+ };
2454
2515
  }
2455
2516
 
2456
2517
  /**
@@ -2656,7 +2717,9 @@ export function configuredModels(settings: Settings): ConfiguredModel[] {
2656
2717
  : { contextWindowTokens: model.contextWindowTokens }),
2657
2718
  ...(model.effectiveContextWindowTokens === undefined
2658
2719
  ? {}
2659
- : { effectiveContextWindowTokens: model.effectiveContextWindowTokens }),
2720
+ : {
2721
+ effectiveContextWindowTokens: model.effectiveContextWindowTokens,
2722
+ }),
2660
2723
  ...(model.autoCompactTokenLimit === undefined
2661
2724
  ? {}
2662
2725
  : { autoCompactTokenLimit: model.autoCompactTokenLimit }),
@@ -3435,7 +3498,9 @@ export function parseMcpServers(raw: string | undefined): unknown[] | undefined
3435
3498
  return parsed;
3436
3499
  } catch (error) {
3437
3500
  const message = error instanceof Error ? error.message : String(error);
3438
- throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}`, { cause: error });
3501
+ throw new Error(`OPENGENI_MCP_SERVERS must be a JSON array: ${message}`, {
3502
+ cause: error,
3503
+ });
3439
3504
  }
3440
3505
  }
3441
3506
 
@@ -3586,6 +3651,53 @@ export function parseIntegrationsOauthClientsJson(
3586
3651
  return out;
3587
3652
  }
3588
3653
 
3654
+ export const SocialOAuthClientConfigSchema = z.object({
3655
+ clientId: z.string().min(1),
3656
+ clientSecret: z.string().min(1).optional(),
3657
+ });
3658
+ export type SocialOAuthClientConfig = z.infer<typeof SocialOAuthClientConfigSchema>;
3659
+
3660
+ const SOCIAL_OAUTH_PROVIDER_IDS = ["x", "reddit"] as const;
3661
+
3662
+ export function parseSocialOauthClientsJson(
3663
+ raw: string | undefined,
3664
+ ): Partial<Record<(typeof SOCIAL_OAUTH_PROVIDER_IDS)[number], SocialOAuthClientConfig>> {
3665
+ if (!raw?.trim() || raw.trim() === "{}") {
3666
+ return {};
3667
+ }
3668
+ let parsed: unknown;
3669
+ try {
3670
+ parsed = JSON.parse(raw);
3671
+ } catch (error) {
3672
+ const message = error instanceof Error ? error.message : String(error);
3673
+ throw new Error(`OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON must be valid JSON: ${message}`, {
3674
+ cause: error,
3675
+ });
3676
+ }
3677
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3678
+ throw new Error(
3679
+ "OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON must be a JSON object keyed by social provider id",
3680
+ );
3681
+ }
3682
+ const out: Partial<Record<(typeof SOCIAL_OAUTH_PROVIDER_IDS)[number], SocialOAuthClientConfig>> =
3683
+ {};
3684
+ for (const [key, value] of Object.entries(parsed)) {
3685
+ if (!SOCIAL_OAUTH_PROVIDER_IDS.includes(key as (typeof SOCIAL_OAUTH_PROVIDER_IDS)[number])) {
3686
+ throw new Error(
3687
+ `OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON provider ${key} is not supported (expected: ${SOCIAL_OAUTH_PROVIDER_IDS.join(", ")})`,
3688
+ );
3689
+ }
3690
+ const result = SocialOAuthClientConfigSchema.safeParse(value);
3691
+ if (!result.success) {
3692
+ throw new Error(
3693
+ `OPENGENI_SOCIAL_OAUTH_CLIENTS_JSON client for ${key} is invalid: ${result.error.message}`,
3694
+ );
3695
+ }
3696
+ out[key as (typeof SOCIAL_OAUTH_PROVIDER_IDS)[number]] = result.data;
3697
+ }
3698
+ return out;
3699
+ }
3700
+
3589
3701
  export function parseStaticUsageLimitsJson(raw: string): StaticUsageLimitsConfig {
3590
3702
  if (!raw.trim() || raw.trim() === "{}") {
3591
3703
  return {};
@@ -3821,6 +3933,11 @@ function validateSettings(settings: Settings): void {
3821
3933
  );
3822
3934
  }
3823
3935
  if (settings.slackClientId) {
3936
+ if (!settings.slackSigningSecret) {
3937
+ throw new Error(
3938
+ "OPENGENI_SLACK_SIGNING_SECRET is required when the OpenGeni Slack app is configured",
3939
+ );
3940
+ }
3824
3941
  if (!settings.publicBaseUrl) {
3825
3942
  throw new Error(
3826
3943
  "OPENGENI_PUBLIC_BASE_URL is required when the OpenGeni Slack app is configured",
@@ -3866,6 +3983,7 @@ function validateSettings(settings: Settings): void {
3866
3983
  }
3867
3984
  }
3868
3985
  parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
3986
+ parseSocialOauthClientsJson(settings.socialOauthClientsJson);
3869
3987
  if (
3870
3988
  settings.productAccessMode === "configured" &&
3871
3989
  !["local", "test"].includes(settings.environment) &&