@wopr-network/platform-core 1.14.6 → 1.14.8

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/.env.example CHANGED
@@ -14,6 +14,16 @@ AFFILIATE_BASE_URL=https://wopr.network/join?ref=
14
14
  # internal network. Enabling on public deployments weakens SSRF protections.
15
15
  # ALLOW_PRIVATE_NODE_HOSTS=false
16
16
 
17
+ # --- Provider API probe URLs ---
18
+ # Override the default health-check/validation endpoints per provider.
19
+ # Useful for air-gapped, proxied, or self-hosted deployments.
20
+ # ANTHROPIC_API_URL=https://api.anthropic.com/v1/models
21
+ # OPENAI_API_URL=https://api.openai.com/v1/models
22
+ # GOOGLE_API_URL=https://generativelanguage.googleapis.com/v1/models
23
+ # DISCORD_API_URL=https://discord.com/api/v10/users/@me
24
+ # ELEVENLABS_API_URL=https://api.elevenlabs.io/v1/user
25
+ # DEEPGRAM_API_URL=https://api.deepgram.com/v1/projects
26
+
17
27
  # --- Affiliate billing caps ---
18
28
 
19
29
  # Maximum number of referrals an affiliate can earn credit for in a 30-day window (default: 20)
@@ -2,5 +2,9 @@ import type { Provider } from "../security/types.js";
2
2
  /**
3
3
  * Base API URLs used to validate provider keys.
4
4
  * Centralised here so every consumer references one source of truth.
5
+ *
6
+ * Each URL can be overridden via env var for proxied / air-gapped / self-hosted deployments:
7
+ * ANTHROPIC_API_URL, OPENAI_API_URL, GOOGLE_API_URL,
8
+ * DISCORD_API_URL, ELEVENLABS_API_URL, DEEPGRAM_API_URL
5
9
  */
6
10
  export declare const PROVIDER_API_URLS: Record<Provider, string>;
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Base API URLs used to validate provider keys.
3
3
  * Centralised here so every consumer references one source of truth.
4
+ *
5
+ * Each URL can be overridden via env var for proxied / air-gapped / self-hosted deployments:
6
+ * ANTHROPIC_API_URL, OPENAI_API_URL, GOOGLE_API_URL,
7
+ * DISCORD_API_URL, ELEVENLABS_API_URL, DEEPGRAM_API_URL
4
8
  */
5
9
  export const PROVIDER_API_URLS = {
6
- anthropic: "https://api.anthropic.com/v1/models",
7
- openai: "https://api.openai.com/v1/models",
8
- google: "https://generativelanguage.googleapis.com/v1/models",
9
- discord: "https://discord.com/api/v10/users/@me",
10
- elevenlabs: "https://api.elevenlabs.io/v1/user",
11
- deepgram: "https://api.deepgram.com/v1/projects",
10
+ anthropic: process.env.ANTHROPIC_API_URL || "https://api.anthropic.com/v1/models",
11
+ openai: process.env.OPENAI_API_URL || "https://api.openai.com/v1/models",
12
+ google: process.env.GOOGLE_API_URL || "https://generativelanguage.googleapis.com/v1/models",
13
+ discord: process.env.DISCORD_API_URL || "https://discord.com/api/v10/users/@me",
14
+ elevenlabs: process.env.ELEVENLABS_API_URL || "https://api.elevenlabs.io/v1/user",
15
+ deepgram: process.env.DEEPGRAM_API_URL || "https://api.deepgram.com/v1/projects",
12
16
  };
@@ -14,10 +14,10 @@ export type DrizzleDb = PgDatabase<PgQueryResultHKT, PlatformSchema>;
14
14
  export type PlatformDb = DrizzleDb;
15
15
  /** Create a Drizzle database instance wrapping the given pg.Pool. */
16
16
  export declare function createDb(pool: Pool): PlatformDb;
17
- export { schema };
18
17
  export type { SQL } from "drizzle-orm";
19
18
  export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
20
19
  export { pgTable, text } from "drizzle-orm/pg-core";
21
20
  export type { AuthUser, IAuthUserRepository } from "./auth-user-repository.js";
22
21
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
23
22
  export { creditColumn } from "./credit-column.js";
23
+ export { schema };
package/dist/db/index.js CHANGED
@@ -4,7 +4,6 @@ import * as schema from "./schema/index.js";
4
4
  export function createDb(pool) {
5
5
  return drizzle(pool, { schema });
6
6
  }
7
- export { schema };
8
7
  // Re-export commonly used drizzle-orm operators so consumers using pnpm link
9
8
  // resolve them from the same drizzle-orm instance as the schema tables.
10
9
  export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
@@ -12,3 +11,4 @@ export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, l
12
11
  export { pgTable, text } from "drizzle-orm/pg-core";
13
12
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
14
13
  export { creditColumn } from "./credit-column.js";
14
+ export { schema };
@@ -1,8 +1,6 @@
1
1
  import type { NodeStatus } from "./node-state-machine.js";
2
2
  import type { NewProvisioningNode, Node, NodeRegistration, NodeTransition, ProvisionDataUpdate, SelfHostedNodeRegistration } from "./repository-types.js";
3
- export type { Node, NodeTransition };
4
- export type { NodeRegistration, SelfHostedNodeRegistration };
5
- export type { NewProvisioningNode, ProvisionDataUpdate };
3
+ export type { NewProvisioningNode, Node, NodeRegistration, NodeTransition, ProvisionDataUpdate, SelfHostedNodeRegistration, };
6
4
  export interface INodeRepository {
7
5
  getById(id: string): Promise<Node | null>;
8
6
  getBySecret(secret: string): Promise<Node | null>;
@@ -41,8 +41,8 @@ _[PENDING: Describe what fixed the issue and how service was restored.]_`,
41
41
  actionItems: `| Action | Owner | Due Date | Priority |
42
42
  |--------|-------|----------|----------|
43
43
  | _[PENDING: Add action item]_ | _[PENDING: Owner]_ | _[PENDING: Date]_ | P1 |
44
- | Improve detection alert thresholds | on-call-engineer | TBD | P2 |
45
- | Add runbook link to alert notification | on-call-engineer | TBD | P2 |`,
44
+ | Improve detection alert thresholds | on-call-engineer | ${actionItemDueDate(incident)} | P2 |
45
+ | Add runbook link to alert notification | on-call-engineer | ${actionItemDueDate(incident)} | P2 |`,
46
46
  lessonsLearned: `**What went well:**
47
47
  - _[PENDING: What worked well in detection and response?]_
48
48
 
@@ -124,3 +124,9 @@ function formatDuration(ms) {
124
124
  return `${hours}h`;
125
125
  return `${hours}h ${remainingMinutes}m`;
126
126
  }
127
+ function actionItemDueDate(incident) {
128
+ const base = incident.resolvedAt ?? incident.startedAt;
129
+ const offsetDays = incident.severity === "SEV1" ? 7 : 14;
130
+ const due = new Date(base.getTime() + offsetDays * 24 * 60 * 60 * 1000);
131
+ return due.toISOString().split("T")[0];
132
+ }
@@ -101,4 +101,39 @@ describe("generatePostMortemTemplate", () => {
101
101
  expect(report.sections.actionItems).not.toContain("TODO");
102
102
  expect(report.sections.lessonsLearned).not.toContain("TODO");
103
103
  });
104
+ it("action items derive due dates from resolvedAt for resolved incidents", () => {
105
+ const report = generatePostMortemTemplate(makeIncident({
106
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
107
+ severity: "SEV1",
108
+ }));
109
+ // SEV1: 7-day offset from resolvedAt → 2026-01-22
110
+ expect(report.sections.actionItems).toContain("2026-01-22");
111
+ expect(report.sections.actionItems).not.toContain("TBD");
112
+ });
113
+ it("action items derive due dates from startedAt for ongoing incidents", () => {
114
+ const report = generatePostMortemTemplate(makeIncident({
115
+ resolvedAt: null,
116
+ startedAt: new Date("2026-01-15T12:00:00Z"),
117
+ severity: "SEV1",
118
+ }));
119
+ // SEV1: 7-day offset from startedAt → 2026-01-22
120
+ expect(report.sections.actionItems).toContain("2026-01-22");
121
+ expect(report.sections.actionItems).not.toContain("TBD");
122
+ });
123
+ it("SEV2 action items use 14-day offset", () => {
124
+ const report = generatePostMortemTemplate(makeIncident({
125
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
126
+ severity: "SEV2",
127
+ }));
128
+ // SEV2: 14-day offset from resolvedAt → 2026-01-29
129
+ expect(report.sections.actionItems).toContain("2026-01-29");
130
+ });
131
+ it("SEV3 action items use 14-day offset", () => {
132
+ const report = generatePostMortemTemplate(makeIncident({
133
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
134
+ severity: "SEV3",
135
+ }));
136
+ // SEV3: 14-day offset from resolvedAt → 2026-01-29
137
+ expect(report.sections.actionItems).toContain("2026-01-29");
138
+ });
104
139
  });
@@ -1,15 +1,20 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { PROVIDER_API_URLS } from "../config/provider-endpoints.js";
3
2
  import { PROVIDER_ENDPOINTS, validateProviderKey } from "./key-validation.js";
4
3
  describe("key-validation", () => {
5
4
  describe("PROVIDER_API_URLS", () => {
6
- it("exports a URL for every supported provider", () => {
7
- expect(PROVIDER_API_URLS.anthropic).toBe("https://api.anthropic.com/v1/models");
8
- expect(PROVIDER_API_URLS.openai).toBe("https://api.openai.com/v1/models");
9
- expect(PROVIDER_API_URLS.google).toBe("https://generativelanguage.googleapis.com/v1/models");
10
- expect(PROVIDER_API_URLS.discord).toBe("https://discord.com/api/v10/users/@me");
11
- expect(PROVIDER_API_URLS.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
12
- expect(PROVIDER_API_URLS.deepgram).toBe("https://api.deepgram.com/v1/projects");
5
+ afterEach(() => {
6
+ vi.unstubAllEnvs();
7
+ vi.resetModules();
8
+ });
9
+ it("exports a URL for every supported provider", async () => {
10
+ vi.resetModules();
11
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
12
+ expect(urls.anthropic).toBe("https://api.anthropic.com/v1/models");
13
+ expect(urls.openai).toBe("https://api.openai.com/v1/models");
14
+ expect(urls.google).toBe("https://generativelanguage.googleapis.com/v1/models");
15
+ expect(urls.discord).toBe("https://discord.com/api/v10/users/@me");
16
+ expect(urls.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
17
+ expect(urls.deepgram).toBe("https://api.deepgram.com/v1/projects");
13
18
  });
14
19
  });
15
20
  describe("PROVIDER_ENDPOINTS", () => {
@@ -84,4 +89,56 @@ describe("key-validation", () => {
84
89
  }));
85
90
  });
86
91
  });
92
+ describe("PROVIDER_API_URLS env overrides", () => {
93
+ afterEach(() => {
94
+ vi.unstubAllEnvs();
95
+ vi.resetModules();
96
+ });
97
+ it("uses ANTHROPIC_API_URL when set", async () => {
98
+ vi.stubEnv("ANTHROPIC_API_URL", "https://custom-anthropic.example.com/v1/models");
99
+ vi.resetModules();
100
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
101
+ expect(urls.anthropic).toBe("https://custom-anthropic.example.com/v1/models");
102
+ });
103
+ it("uses OPENAI_API_URL when set", async () => {
104
+ vi.stubEnv("OPENAI_API_URL", "https://custom-openai.example.com/v1/models");
105
+ vi.resetModules();
106
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
107
+ expect(urls.openai).toBe("https://custom-openai.example.com/v1/models");
108
+ });
109
+ it("uses GOOGLE_API_URL when set", async () => {
110
+ vi.stubEnv("GOOGLE_API_URL", "https://custom-google.example.com/v1/models");
111
+ vi.resetModules();
112
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
113
+ expect(urls.google).toBe("https://custom-google.example.com/v1/models");
114
+ });
115
+ it("uses DISCORD_API_URL when set", async () => {
116
+ vi.stubEnv("DISCORD_API_URL", "https://custom-discord.example.com/api/v10/users/@me");
117
+ vi.resetModules();
118
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
119
+ expect(urls.discord).toBe("https://custom-discord.example.com/api/v10/users/@me");
120
+ });
121
+ it("uses ELEVENLABS_API_URL when set", async () => {
122
+ vi.stubEnv("ELEVENLABS_API_URL", "https://custom-elevenlabs.example.com/v1/user");
123
+ vi.resetModules();
124
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
125
+ expect(urls.elevenlabs).toBe("https://custom-elevenlabs.example.com/v1/user");
126
+ });
127
+ it("uses DEEPGRAM_API_URL when set", async () => {
128
+ vi.stubEnv("DEEPGRAM_API_URL", "https://custom-deepgram.example.com/v1/projects");
129
+ vi.resetModules();
130
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
131
+ expect(urls.deepgram).toBe("https://custom-deepgram.example.com/v1/projects");
132
+ });
133
+ it("falls back to defaults when env vars are not set", async () => {
134
+ vi.resetModules();
135
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
136
+ expect(urls.anthropic).toBe("https://api.anthropic.com/v1/models");
137
+ expect(urls.openai).toBe("https://api.openai.com/v1/models");
138
+ expect(urls.google).toBe("https://generativelanguage.googleapis.com/v1/models");
139
+ expect(urls.discord).toBe("https://discord.com/api/v10/users/@me");
140
+ expect(urls.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
141
+ expect(urls.deepgram).toBe("https://api.deepgram.com/v1/projects");
142
+ });
143
+ });
87
144
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.14.6",
3
+ "version": "1.14.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -3,12 +3,16 @@ import type { Provider } from "../security/types.js";
3
3
  /**
4
4
  * Base API URLs used to validate provider keys.
5
5
  * Centralised here so every consumer references one source of truth.
6
+ *
7
+ * Each URL can be overridden via env var for proxied / air-gapped / self-hosted deployments:
8
+ * ANTHROPIC_API_URL, OPENAI_API_URL, GOOGLE_API_URL,
9
+ * DISCORD_API_URL, ELEVENLABS_API_URL, DEEPGRAM_API_URL
6
10
  */
7
11
  export const PROVIDER_API_URLS: Record<Provider, string> = {
8
- anthropic: "https://api.anthropic.com/v1/models",
9
- openai: "https://api.openai.com/v1/models",
10
- google: "https://generativelanguage.googleapis.com/v1/models",
11
- discord: "https://discord.com/api/v10/users/@me",
12
- elevenlabs: "https://api.elevenlabs.io/v1/user",
13
- deepgram: "https://api.deepgram.com/v1/projects",
12
+ anthropic: process.env.ANTHROPIC_API_URL || "https://api.anthropic.com/v1/models",
13
+ openai: process.env.OPENAI_API_URL || "https://api.openai.com/v1/models",
14
+ google: process.env.GOOGLE_API_URL || "https://generativelanguage.googleapis.com/v1/models",
15
+ discord: process.env.DISCORD_API_URL || "https://discord.com/api/v10/users/@me",
16
+ elevenlabs: process.env.ELEVENLABS_API_URL || "https://api.elevenlabs.io/v1/user",
17
+ deepgram: process.env.DEEPGRAM_API_URL || "https://api.deepgram.com/v1/projects",
14
18
  };
package/src/db/index.ts CHANGED
@@ -23,8 +23,6 @@ export function createDb(pool: Pool): PlatformDb {
23
23
  return drizzle(pool, { schema }) as unknown as PlatformDb;
24
24
  }
25
25
 
26
- export { schema };
27
-
28
26
  export type { SQL } from "drizzle-orm";
29
27
  // Re-export commonly used drizzle-orm operators so consumers using pnpm link
30
28
  // resolve them from the same drizzle-orm instance as the schema tables.
@@ -34,3 +32,4 @@ export { pgTable, text } from "drizzle-orm/pg-core";
34
32
  export type { AuthUser, IAuthUserRepository } from "./auth-user-repository.js";
35
33
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
36
34
  export { creditColumn } from "./credit-column.js";
35
+ export { schema };
@@ -9,9 +9,14 @@ import type {
9
9
  } from "./repository-types.js";
10
10
 
11
11
  // Re-export domain types so existing consumers don't break
12
- export type { Node, NodeTransition };
13
- export type { NodeRegistration, SelfHostedNodeRegistration };
14
- export type { NewProvisioningNode, ProvisionDataUpdate };
12
+ export type {
13
+ NewProvisioningNode,
14
+ Node,
15
+ NodeRegistration,
16
+ NodeTransition,
17
+ ProvisionDataUpdate,
18
+ SelfHostedNodeRegistration,
19
+ };
15
20
 
16
21
  export interface INodeRepository {
17
22
  getById(id: string): Promise<Node | null>;
@@ -121,4 +121,51 @@ describe("generatePostMortemTemplate", () => {
121
121
  expect(report.sections.actionItems).not.toContain("TODO");
122
122
  expect(report.sections.lessonsLearned).not.toContain("TODO");
123
123
  });
124
+
125
+ it("action items derive due dates from resolvedAt for resolved incidents", () => {
126
+ const report = generatePostMortemTemplate(
127
+ makeIncident({
128
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
129
+ severity: "SEV1",
130
+ }),
131
+ );
132
+ // SEV1: 7-day offset from resolvedAt → 2026-01-22
133
+ expect(report.sections.actionItems).toContain("2026-01-22");
134
+ expect(report.sections.actionItems).not.toContain("TBD");
135
+ });
136
+
137
+ it("action items derive due dates from startedAt for ongoing incidents", () => {
138
+ const report = generatePostMortemTemplate(
139
+ makeIncident({
140
+ resolvedAt: null,
141
+ startedAt: new Date("2026-01-15T12:00:00Z"),
142
+ severity: "SEV1",
143
+ }),
144
+ );
145
+ // SEV1: 7-day offset from startedAt → 2026-01-22
146
+ expect(report.sections.actionItems).toContain("2026-01-22");
147
+ expect(report.sections.actionItems).not.toContain("TBD");
148
+ });
149
+
150
+ it("SEV2 action items use 14-day offset", () => {
151
+ const report = generatePostMortemTemplate(
152
+ makeIncident({
153
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
154
+ severity: "SEV2",
155
+ }),
156
+ );
157
+ // SEV2: 14-day offset from resolvedAt → 2026-01-29
158
+ expect(report.sections.actionItems).toContain("2026-01-29");
159
+ });
160
+
161
+ it("SEV3 action items use 14-day offset", () => {
162
+ const report = generatePostMortemTemplate(
163
+ makeIncident({
164
+ resolvedAt: new Date("2026-01-15T13:00:00Z"),
165
+ severity: "SEV3",
166
+ }),
167
+ );
168
+ // SEV3: 14-day offset from resolvedAt → 2026-01-29
169
+ expect(report.sections.actionItems).toContain("2026-01-29");
170
+ });
124
171
  });
@@ -79,8 +79,8 @@ _[PENDING: Describe what fixed the issue and how service was restored.]_`,
79
79
  actionItems: `| Action | Owner | Due Date | Priority |
80
80
  |--------|-------|----------|----------|
81
81
  | _[PENDING: Add action item]_ | _[PENDING: Owner]_ | _[PENDING: Date]_ | P1 |
82
- | Improve detection alert thresholds | on-call-engineer | TBD | P2 |
83
- | Add runbook link to alert notification | on-call-engineer | TBD | P2 |`,
82
+ | Improve detection alert thresholds | on-call-engineer | ${actionItemDueDate(incident)} | P2 |
83
+ | Add runbook link to alert notification | on-call-engineer | ${actionItemDueDate(incident)} | P2 |`,
84
84
 
85
85
  lessonsLearned: `**What went well:**
86
86
  - _[PENDING: What worked well in detection and response?]_
@@ -165,3 +165,10 @@ function formatDuration(ms: number): string {
165
165
  if (remainingMinutes === 0) return `${hours}h`;
166
166
  return `${hours}h ${remainingMinutes}m`;
167
167
  }
168
+
169
+ function actionItemDueDate(incident: IncidentSummary): string {
170
+ const base = incident.resolvedAt ?? incident.startedAt;
171
+ const offsetDays = incident.severity === "SEV1" ? 7 : 14;
172
+ const due = new Date(base.getTime() + offsetDays * 24 * 60 * 60 * 1000);
173
+ return due.toISOString().split("T")[0];
174
+ }
@@ -1,16 +1,22 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { PROVIDER_API_URLS } from "../config/provider-endpoints.js";
3
2
  import { PROVIDER_ENDPOINTS, validateProviderKey } from "./key-validation.js";
4
3
 
5
4
  describe("key-validation", () => {
6
5
  describe("PROVIDER_API_URLS", () => {
7
- it("exports a URL for every supported provider", () => {
8
- expect(PROVIDER_API_URLS.anthropic).toBe("https://api.anthropic.com/v1/models");
9
- expect(PROVIDER_API_URLS.openai).toBe("https://api.openai.com/v1/models");
10
- expect(PROVIDER_API_URLS.google).toBe("https://generativelanguage.googleapis.com/v1/models");
11
- expect(PROVIDER_API_URLS.discord).toBe("https://discord.com/api/v10/users/@me");
12
- expect(PROVIDER_API_URLS.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
13
- expect(PROVIDER_API_URLS.deepgram).toBe("https://api.deepgram.com/v1/projects");
6
+ afterEach(() => {
7
+ vi.unstubAllEnvs();
8
+ vi.resetModules();
9
+ });
10
+
11
+ it("exports a URL for every supported provider", async () => {
12
+ vi.resetModules();
13
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
14
+ expect(urls.anthropic).toBe("https://api.anthropic.com/v1/models");
15
+ expect(urls.openai).toBe("https://api.openai.com/v1/models");
16
+ expect(urls.google).toBe("https://generativelanguage.googleapis.com/v1/models");
17
+ expect(urls.discord).toBe("https://discord.com/api/v10/users/@me");
18
+ expect(urls.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
19
+ expect(urls.deepgram).toBe("https://api.deepgram.com/v1/projects");
14
20
  });
15
21
  });
16
22
 
@@ -108,4 +114,64 @@ describe("key-validation", () => {
108
114
  );
109
115
  });
110
116
  });
117
+
118
+ describe("PROVIDER_API_URLS env overrides", () => {
119
+ afterEach(() => {
120
+ vi.unstubAllEnvs();
121
+ vi.resetModules();
122
+ });
123
+
124
+ it("uses ANTHROPIC_API_URL when set", async () => {
125
+ vi.stubEnv("ANTHROPIC_API_URL", "https://custom-anthropic.example.com/v1/models");
126
+ vi.resetModules();
127
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
128
+ expect(urls.anthropic).toBe("https://custom-anthropic.example.com/v1/models");
129
+ });
130
+
131
+ it("uses OPENAI_API_URL when set", async () => {
132
+ vi.stubEnv("OPENAI_API_URL", "https://custom-openai.example.com/v1/models");
133
+ vi.resetModules();
134
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
135
+ expect(urls.openai).toBe("https://custom-openai.example.com/v1/models");
136
+ });
137
+
138
+ it("uses GOOGLE_API_URL when set", async () => {
139
+ vi.stubEnv("GOOGLE_API_URL", "https://custom-google.example.com/v1/models");
140
+ vi.resetModules();
141
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
142
+ expect(urls.google).toBe("https://custom-google.example.com/v1/models");
143
+ });
144
+
145
+ it("uses DISCORD_API_URL when set", async () => {
146
+ vi.stubEnv("DISCORD_API_URL", "https://custom-discord.example.com/api/v10/users/@me");
147
+ vi.resetModules();
148
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
149
+ expect(urls.discord).toBe("https://custom-discord.example.com/api/v10/users/@me");
150
+ });
151
+
152
+ it("uses ELEVENLABS_API_URL when set", async () => {
153
+ vi.stubEnv("ELEVENLABS_API_URL", "https://custom-elevenlabs.example.com/v1/user");
154
+ vi.resetModules();
155
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
156
+ expect(urls.elevenlabs).toBe("https://custom-elevenlabs.example.com/v1/user");
157
+ });
158
+
159
+ it("uses DEEPGRAM_API_URL when set", async () => {
160
+ vi.stubEnv("DEEPGRAM_API_URL", "https://custom-deepgram.example.com/v1/projects");
161
+ vi.resetModules();
162
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
163
+ expect(urls.deepgram).toBe("https://custom-deepgram.example.com/v1/projects");
164
+ });
165
+
166
+ it("falls back to defaults when env vars are not set", async () => {
167
+ vi.resetModules();
168
+ const { PROVIDER_API_URLS: urls } = await import("../config/provider-endpoints.js");
169
+ expect(urls.anthropic).toBe("https://api.anthropic.com/v1/models");
170
+ expect(urls.openai).toBe("https://api.openai.com/v1/models");
171
+ expect(urls.google).toBe("https://generativelanguage.googleapis.com/v1/models");
172
+ expect(urls.discord).toBe("https://discord.com/api/v10/users/@me");
173
+ expect(urls.elevenlabs).toBe("https://api.elevenlabs.io/v1/user");
174
+ expect(urls.deepgram).toBe("https://api.deepgram.com/v1/projects");
175
+ });
176
+ });
111
177
  });