@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 +10 -0
- package/dist/config/provider-endpoints.d.ts +4 -0
- package/dist/config/provider-endpoints.js +10 -6
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/fleet/node-repository.d.ts +1 -3
- package/dist/monetization/incident/postmortem.js +8 -2
- package/dist/monetization/incident/postmortem.test.js +35 -0
- package/dist/security/key-validation.test.js +65 -8
- package/package.json +1 -1
- package/src/config/provider-endpoints.ts +10 -6
- package/src/db/index.ts +1 -2
- package/src/fleet/node-repository.ts +8 -3
- package/src/monetization/incident/postmortem.test.ts +47 -0
- package/src/monetization/incident/postmortem.ts +9 -2
- package/src/security/key-validation.test.ts +74 -8
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
|
};
|
package/dist/db/index.d.ts
CHANGED
|
@@ -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 |
|
|
45
|
-
| Add runbook link to alert notification | on-call-engineer |
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
@@ -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 {
|
|
13
|
-
|
|
14
|
-
|
|
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 |
|
|
83
|
-
| Add runbook link to alert notification | on-call-engineer |
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
});
|