@wopr-network/platform-core 1.70.0 → 1.72.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.
- package/dist/server/boot-config.d.ts +11 -1
- package/dist/server/container.d.ts +2 -0
- package/dist/server/container.js +21 -11
- package/dist/server/services/__tests__/hot-pool.test.d.ts +6 -0
- package/dist/server/services/__tests__/hot-pool.test.js +26 -0
- package/dist/server/services/__tests__/in-memory-pool-repository.d.ts +21 -0
- package/dist/server/services/__tests__/in-memory-pool-repository.js +57 -0
- package/dist/server/services/__tests__/pool-repository.test.d.ts +7 -0
- package/dist/server/services/__tests__/pool-repository.test.js +108 -0
- package/dist/server/test-container.js +14 -0
- package/dist/trpc/index.d.ts +4 -1
- package/dist/trpc/index.js +4 -1
- package/dist/trpc/init.d.ts +5 -0
- package/dist/trpc/init.js +28 -0
- package/dist/trpc/routers/page-context.d.ts +37 -0
- package/dist/trpc/routers/page-context.js +41 -0
- package/dist/trpc/routers/profile.d.ts +71 -0
- package/dist/trpc/routers/profile.js +68 -0
- package/dist/trpc/routers/settings.d.ts +85 -0
- package/dist/trpc/routers/settings.js +73 -0
- package/package.json +1 -1
- package/src/server/boot-config.ts +12 -1
- package/src/server/container.ts +22 -12
- package/src/server/services/__tests__/hot-pool.test.ts +30 -0
- package/src/server/services/__tests__/in-memory-pool-repository.ts +66 -0
- package/src/server/services/__tests__/pool-repository.test.ts +135 -0
- package/src/server/test-container.ts +15 -0
- package/src/trpc/index.ts +4 -0
- package/src/trpc/init.ts +28 -0
- package/src/trpc/routers/page-context.ts +57 -0
- package/src/trpc/routers/profile.ts +94 -0
- package/src/trpc/routers/settings.ts +97 -0
|
@@ -20,8 +20,18 @@ export interface RoutePlugin {
|
|
|
20
20
|
export interface BootConfig {
|
|
21
21
|
/** Short product identifier (e.g. "paperclip", "wopr", "holyship"). */
|
|
22
22
|
slug: string;
|
|
23
|
-
/** PostgreSQL connection string. */
|
|
23
|
+
/** PostgreSQL connection string. Required unless `pool` is provided. */
|
|
24
24
|
databaseUrl: string;
|
|
25
|
+
/**
|
|
26
|
+
* Pre-created PostgreSQL connection pool. When provided, buildContainer
|
|
27
|
+
* reuses this pool and skips pool creation + Drizzle migrations. The
|
|
28
|
+
* caller is responsible for running their own migrations before calling
|
|
29
|
+
* buildContainer.
|
|
30
|
+
*
|
|
31
|
+
* Use this when the product has its own migration set (e.g. wopr-platform
|
|
32
|
+
* generates migrations locally from the shared schema).
|
|
33
|
+
*/
|
|
34
|
+
pool?: import("pg").Pool;
|
|
25
35
|
/** Bind host (default "0.0.0.0"). */
|
|
26
36
|
host?: string;
|
|
27
37
|
/** Bind port (default 3001). */
|
|
@@ -18,6 +18,7 @@ import type { FleetManager } from "../fleet/fleet-manager.js";
|
|
|
18
18
|
import type { IProfileStore } from "../fleet/profile-store.js";
|
|
19
19
|
import type { IServiceKeyRepository } from "../gateway/service-key-repository.js";
|
|
20
20
|
import type { ProductConfig } from "../product-config/repository-types.js";
|
|
21
|
+
import type { ProductConfigService } from "../product-config/service.js";
|
|
21
22
|
import type { ProxyManagerInterface } from "../proxy/types.js";
|
|
22
23
|
import type { IOrgMemberRepository } from "../tenancy/org-member-repository.js";
|
|
23
24
|
import type { OrgService } from "../tenancy/org-service.js";
|
|
@@ -68,6 +69,7 @@ export interface PlatformContainer {
|
|
|
68
69
|
db: DrizzleDb;
|
|
69
70
|
pool: Pool;
|
|
70
71
|
productConfig: ProductConfig;
|
|
72
|
+
productConfigService: ProductConfigService;
|
|
71
73
|
creditLedger: ILedger;
|
|
72
74
|
orgMemberRepo: IOrgMemberRepository;
|
|
73
75
|
orgService: OrgService;
|
package/dist/server/container.js
CHANGED
|
@@ -20,23 +20,32 @@
|
|
|
20
20
|
* `bootConfig.features`. Disabled features yield `null`.
|
|
21
21
|
*/
|
|
22
22
|
export async function buildContainer(bootConfig) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// 1. Database pool — reuse existing or create new
|
|
24
|
+
let pool;
|
|
25
|
+
if (bootConfig.pool) {
|
|
26
|
+
pool = bootConfig.pool;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
if (!bootConfig.databaseUrl) {
|
|
30
|
+
throw new Error("buildContainer: databaseUrl is required when pool is not provided");
|
|
31
|
+
}
|
|
32
|
+
const { Pool: PgPool } = await import("pg");
|
|
33
|
+
pool = new PgPool({ connectionString: bootConfig.databaseUrl });
|
|
25
34
|
}
|
|
26
|
-
// 1. Database pool
|
|
27
|
-
const { Pool: PgPool } = await import("pg");
|
|
28
|
-
const pool = new PgPool({ connectionString: bootConfig.databaseUrl });
|
|
29
35
|
// 2. Drizzle ORM instance
|
|
30
36
|
const { createDb } = await import("../db/index.js");
|
|
31
37
|
const db = createDb(pool);
|
|
32
|
-
// 3. Run Drizzle migrations
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
// 3. Run Drizzle migrations (skip when caller provided their own pool —
|
|
39
|
+
// they are responsible for running product-specific migrations first)
|
|
40
|
+
if (!bootConfig.pool) {
|
|
41
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
42
|
+
const path = await import("node:path");
|
|
43
|
+
const migrationsFolder = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../drizzle");
|
|
44
|
+
await migrate(db, { migrationsFolder });
|
|
45
|
+
}
|
|
37
46
|
// 4. Bootstrap product config from DB (auto-seeds from presets if needed)
|
|
38
47
|
const { platformBoot } = await import("../product-config/boot.js");
|
|
39
|
-
const { config: productConfig } = await platformBoot({ slug: bootConfig.slug, db });
|
|
48
|
+
const { config: productConfig, service: productConfigService } = await platformBoot({ slug: bootConfig.slug, db });
|
|
40
49
|
// 5. Credit ledger
|
|
41
50
|
const { DrizzleLedger } = await import("../credits/ledger.js");
|
|
42
51
|
const creditLedger = new DrizzleLedger(db);
|
|
@@ -120,6 +129,7 @@ export async function buildContainer(bootConfig) {
|
|
|
120
129
|
db,
|
|
121
130
|
pool,
|
|
122
131
|
productConfig,
|
|
132
|
+
productConfigService,
|
|
123
133
|
creditLedger,
|
|
124
134
|
orgMemberRepo,
|
|
125
135
|
orgService,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for hot pool service functions (getPoolSize, setPoolSize).
|
|
3
|
+
*
|
|
4
|
+
* Uses InMemoryPoolRepository to test service logic without Docker or DB.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { getPoolSize, setPoolSize } from "../hot-pool.js";
|
|
8
|
+
import { InMemoryPoolRepository } from "./in-memory-pool-repository.js";
|
|
9
|
+
describe("hot pool service", () => {
|
|
10
|
+
describe("getPoolSize / setPoolSize", () => {
|
|
11
|
+
it("returns default pool size", async () => {
|
|
12
|
+
const repo = new InMemoryPoolRepository();
|
|
13
|
+
expect(await getPoolSize(repo)).toBe(2);
|
|
14
|
+
});
|
|
15
|
+
it("sets and reads pool size", async () => {
|
|
16
|
+
const repo = new InMemoryPoolRepository();
|
|
17
|
+
await setPoolSize(repo, 10);
|
|
18
|
+
expect(await getPoolSize(repo)).toBe(10);
|
|
19
|
+
});
|
|
20
|
+
it("pool size of 0 is valid", async () => {
|
|
21
|
+
const repo = new InMemoryPoolRepository();
|
|
22
|
+
await setPoolSize(repo, 0);
|
|
23
|
+
expect(await getPoolSize(repo)).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory IPoolRepository for testing.
|
|
3
|
+
* FIFO claiming, dead instance handling — no DB required.
|
|
4
|
+
*/
|
|
5
|
+
import type { IPoolRepository, PoolInstance } from "../pool-repository.js";
|
|
6
|
+
export declare class InMemoryPoolRepository implements IPoolRepository {
|
|
7
|
+
private poolSize;
|
|
8
|
+
private instances;
|
|
9
|
+
getPoolSize(): Promise<number>;
|
|
10
|
+
setPoolSize(size: number): Promise<void>;
|
|
11
|
+
warmCount(): Promise<number>;
|
|
12
|
+
insertWarm(id: string, containerId: string): Promise<void>;
|
|
13
|
+
listWarm(): Promise<PoolInstance[]>;
|
|
14
|
+
markDead(id: string): Promise<void>;
|
|
15
|
+
deleteDead(): Promise<void>;
|
|
16
|
+
claimWarm(tenantId: string, name: string): Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
containerId: string;
|
|
19
|
+
} | null>;
|
|
20
|
+
updateInstanceStatus(id: string, status: string): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory IPoolRepository for testing.
|
|
3
|
+
* FIFO claiming, dead instance handling — no DB required.
|
|
4
|
+
*/
|
|
5
|
+
export class InMemoryPoolRepository {
|
|
6
|
+
poolSize = 2;
|
|
7
|
+
instances = [];
|
|
8
|
+
async getPoolSize() {
|
|
9
|
+
return this.poolSize;
|
|
10
|
+
}
|
|
11
|
+
async setPoolSize(size) {
|
|
12
|
+
this.poolSize = size;
|
|
13
|
+
}
|
|
14
|
+
async warmCount() {
|
|
15
|
+
return this.instances.filter((i) => i.status === "warm").length;
|
|
16
|
+
}
|
|
17
|
+
async insertWarm(id, containerId) {
|
|
18
|
+
this.instances.push({
|
|
19
|
+
id,
|
|
20
|
+
containerId,
|
|
21
|
+
status: "warm",
|
|
22
|
+
tenantId: null,
|
|
23
|
+
name: null,
|
|
24
|
+
createdAt: new Date(),
|
|
25
|
+
claimedAt: null,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async listWarm() {
|
|
29
|
+
return this.instances.filter((i) => i.status === "warm").map(({ createdAt, claimedAt, ...rest }) => rest);
|
|
30
|
+
}
|
|
31
|
+
async markDead(id) {
|
|
32
|
+
const inst = this.instances.find((i) => i.id === id);
|
|
33
|
+
if (inst)
|
|
34
|
+
inst.status = "dead";
|
|
35
|
+
}
|
|
36
|
+
async deleteDead() {
|
|
37
|
+
this.instances = this.instances.filter((i) => i.status !== "dead");
|
|
38
|
+
}
|
|
39
|
+
async claimWarm(tenantId, name) {
|
|
40
|
+
const warm = this.instances
|
|
41
|
+
.filter((i) => i.status === "warm")
|
|
42
|
+
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
43
|
+
if (warm.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const target = warm[0];
|
|
46
|
+
target.status = "claimed";
|
|
47
|
+
target.tenantId = tenantId;
|
|
48
|
+
target.name = name;
|
|
49
|
+
target.claimedAt = new Date();
|
|
50
|
+
return { id: target.id, containerId: target.containerId };
|
|
51
|
+
}
|
|
52
|
+
async updateInstanceStatus(id, status) {
|
|
53
|
+
const inst = this.instances.find((i) => i.id === id);
|
|
54
|
+
if (inst)
|
|
55
|
+
inst.status = status;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for IPoolRepository interface contract.
|
|
3
|
+
*
|
|
4
|
+
* Uses an in-memory implementation to verify the interface
|
|
5
|
+
* behavior without requiring a real database.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { InMemoryPoolRepository } from "./in-memory-pool-repository.js";
|
|
9
|
+
describe("IPoolRepository (InMemory)", () => {
|
|
10
|
+
it("returns default pool size of 2", async () => {
|
|
11
|
+
const repo = new InMemoryPoolRepository();
|
|
12
|
+
expect(await repo.getPoolSize()).toBe(2);
|
|
13
|
+
});
|
|
14
|
+
it("sets and gets pool size", async () => {
|
|
15
|
+
const repo = new InMemoryPoolRepository();
|
|
16
|
+
await repo.setPoolSize(5);
|
|
17
|
+
expect(await repo.getPoolSize()).toBe(5);
|
|
18
|
+
});
|
|
19
|
+
it("inserts and counts warm instances", async () => {
|
|
20
|
+
const repo = new InMemoryPoolRepository();
|
|
21
|
+
expect(await repo.warmCount()).toBe(0);
|
|
22
|
+
await repo.insertWarm("a", "container-a");
|
|
23
|
+
await repo.insertWarm("b", "container-b");
|
|
24
|
+
expect(await repo.warmCount()).toBe(2);
|
|
25
|
+
});
|
|
26
|
+
it("lists warm instances", async () => {
|
|
27
|
+
const repo = new InMemoryPoolRepository();
|
|
28
|
+
await repo.insertWarm("a", "container-a");
|
|
29
|
+
await repo.insertWarm("b", "container-b");
|
|
30
|
+
const warm = await repo.listWarm();
|
|
31
|
+
expect(warm).toHaveLength(2);
|
|
32
|
+
expect(warm[0].id).toBe("a");
|
|
33
|
+
expect(warm[0].containerId).toBe("container-a");
|
|
34
|
+
expect(warm[0].status).toBe("warm");
|
|
35
|
+
});
|
|
36
|
+
it("claims warm instance FIFO", async () => {
|
|
37
|
+
const repo = new InMemoryPoolRepository();
|
|
38
|
+
await repo.insertWarm("first", "c-first");
|
|
39
|
+
await repo.insertWarm("second", "c-second");
|
|
40
|
+
const claimed = await repo.claimWarm("tenant-1", "my-bot");
|
|
41
|
+
expect(claimed).not.toBeNull();
|
|
42
|
+
expect(claimed?.id).toBe("first");
|
|
43
|
+
expect(claimed?.containerId).toBe("c-first");
|
|
44
|
+
// Warm count drops by 1
|
|
45
|
+
expect(await repo.warmCount()).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
it("returns null when claiming from empty pool", async () => {
|
|
48
|
+
const repo = new InMemoryPoolRepository();
|
|
49
|
+
const result = await repo.claimWarm("tenant-1", "bot");
|
|
50
|
+
expect(result).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it("does not re-claim already claimed instances", async () => {
|
|
53
|
+
const repo = new InMemoryPoolRepository();
|
|
54
|
+
await repo.insertWarm("only", "c-only");
|
|
55
|
+
const first = await repo.claimWarm("t1", "bot1");
|
|
56
|
+
expect(first).not.toBeNull();
|
|
57
|
+
const second = await repo.claimWarm("t2", "bot2");
|
|
58
|
+
expect(second).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
it("marks instances dead", async () => {
|
|
61
|
+
const repo = new InMemoryPoolRepository();
|
|
62
|
+
await repo.insertWarm("a", "c-a");
|
|
63
|
+
await repo.markDead("a");
|
|
64
|
+
expect(await repo.warmCount()).toBe(0);
|
|
65
|
+
const warm = await repo.listWarm();
|
|
66
|
+
expect(warm).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
it("deletes dead instances", async () => {
|
|
69
|
+
const repo = new InMemoryPoolRepository();
|
|
70
|
+
await repo.insertWarm("a", "c-a");
|
|
71
|
+
await repo.insertWarm("b", "c-b");
|
|
72
|
+
await repo.markDead("a");
|
|
73
|
+
await repo.deleteDead();
|
|
74
|
+
// Only 'b' remains (warm)
|
|
75
|
+
expect(await repo.warmCount()).toBe(1);
|
|
76
|
+
const warm = await repo.listWarm();
|
|
77
|
+
expect(warm[0].id).toBe("b");
|
|
78
|
+
});
|
|
79
|
+
it("updates instance status", async () => {
|
|
80
|
+
const repo = new InMemoryPoolRepository();
|
|
81
|
+
await repo.insertWarm("a", "c-a");
|
|
82
|
+
await repo.updateInstanceStatus("a", "provisioning");
|
|
83
|
+
// No longer warm
|
|
84
|
+
expect(await repo.warmCount()).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
it("handles multiple claims in order", async () => {
|
|
87
|
+
const repo = new InMemoryPoolRepository();
|
|
88
|
+
await repo.insertWarm("1", "c-1");
|
|
89
|
+
await repo.insertWarm("2", "c-2");
|
|
90
|
+
await repo.insertWarm("3", "c-3");
|
|
91
|
+
const c1 = await repo.claimWarm("t-a", "bot-a");
|
|
92
|
+
const c2 = await repo.claimWarm("t-b", "bot-b");
|
|
93
|
+
const c3 = await repo.claimWarm("t-c", "bot-c");
|
|
94
|
+
const c4 = await repo.claimWarm("t-d", "bot-d");
|
|
95
|
+
expect(c1?.id).toBe("1");
|
|
96
|
+
expect(c2?.id).toBe("2");
|
|
97
|
+
expect(c3?.id).toBe("3");
|
|
98
|
+
expect(c4).toBeNull();
|
|
99
|
+
expect(await repo.warmCount()).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
it("dead instances are not claimable", async () => {
|
|
102
|
+
const repo = new InMemoryPoolRepository();
|
|
103
|
+
await repo.insertWarm("a", "c-a");
|
|
104
|
+
await repo.markDead("a");
|
|
105
|
+
const result = await repo.claimWarm("t1", "bot");
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* const c = createTestContainer();
|
|
8
8
|
* const c2 = createTestContainer({ creditLedger: myCustomLedger });
|
|
9
9
|
*/
|
|
10
|
+
import { ProductConfigService } from "../product-config/service.js";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Stub factories (satisfy interface contracts with no-op implementations)
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +77,18 @@ function stubProductConfig() {
|
|
|
76
77
|
billing: null,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
function stubProductConfigService() {
|
|
81
|
+
const stubRepo = {
|
|
82
|
+
getBySlug: async () => stubProductConfig(),
|
|
83
|
+
listAll: async () => [],
|
|
84
|
+
upsertProduct: async () => stubProductConfig().product,
|
|
85
|
+
replaceNavItems: async () => { },
|
|
86
|
+
upsertFeatures: async () => { },
|
|
87
|
+
upsertFleetConfig: async () => { },
|
|
88
|
+
upsertBillingConfig: async () => { },
|
|
89
|
+
};
|
|
90
|
+
return new ProductConfigService(stubRepo);
|
|
91
|
+
}
|
|
79
92
|
// ---------------------------------------------------------------------------
|
|
80
93
|
// Public API
|
|
81
94
|
// ---------------------------------------------------------------------------
|
|
@@ -88,6 +101,7 @@ export function createTestContainer(overrides) {
|
|
|
88
101
|
db: {},
|
|
89
102
|
pool: { end: async () => { } },
|
|
90
103
|
productConfig: stubProductConfig(),
|
|
104
|
+
productConfigService: stubProductConfigService(),
|
|
91
105
|
creditLedger: stubLedger(),
|
|
92
106
|
orgMemberRepo: stubOrgMemberRepo(),
|
|
93
107
|
orgService: {},
|
package/dist/trpc/index.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export { createAssertOrgAdminOrOwner } from "./auth-helpers.js";
|
|
|
3
3
|
export { authSocialRouter } from "./auth-social-router.js";
|
|
4
4
|
export { createAdminFleetUpdateRouterFromContainer, createFleetUpdateConfigRouterFromContainer, createNotificationTemplateRouterFromContainer, createOrgRemovePaymentMethodRouterFromContainer, createProductConfigRouterFromContainer, initTrpcFromContainer, } from "./container-factories.js";
|
|
5
5
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
6
|
-
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
6
|
+
export { adminProcedure, createCallerFactory, createTRPCContext, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, type TRPCContext, tenantProcedure, } from "./init.js";
|
|
7
7
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
|
8
8
|
export { createOrgRemovePaymentMethodRouter, type OrgRemovePaymentMethodDeps, } from "./org-remove-payment-method-router.js";
|
|
9
9
|
export { createProductConfigRouter } from "./product-config-router.js";
|
|
10
|
+
export { type PageContextRouterDeps, pageContextRouter, setPageContextRouterDeps } from "./routers/page-context.js";
|
|
11
|
+
export { type ProfileRouterDeps, profileRouter, setProfileRouterDeps } from "./routers/profile.js";
|
|
12
|
+
export { type SettingsRouterDeps, setSettingsRouterDeps, settingsRouter } from "./routers/settings.js";
|
package/dist/trpc/index.js
CHANGED
|
@@ -3,7 +3,10 @@ export { createAssertOrgAdminOrOwner } from "./auth-helpers.js";
|
|
|
3
3
|
export { authSocialRouter } from "./auth-social-router.js";
|
|
4
4
|
export { createAdminFleetUpdateRouterFromContainer, createFleetUpdateConfigRouterFromContainer, createNotificationTemplateRouterFromContainer, createOrgRemovePaymentMethodRouterFromContainer, createProductConfigRouterFromContainer, initTrpcFromContainer, } from "./container-factories.js";
|
|
5
5
|
export { createFleetUpdateConfigRouter } from "./fleet-update-config-router.js";
|
|
6
|
-
export { adminProcedure, createCallerFactory, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
6
|
+
export { adminProcedure, createCallerFactory, createTRPCContext, orgAdminProcedure, orgMemberProcedure, protectedProcedure, publicProcedure, router, setTrpcOrgMemberRepo, tenantProcedure, } from "./init.js";
|
|
7
7
|
export { createNotificationTemplateRouter } from "./notification-template-router.js";
|
|
8
8
|
export { createOrgRemovePaymentMethodRouter, } from "./org-remove-payment-method-router.js";
|
|
9
9
|
export { createProductConfigRouter } from "./product-config-router.js";
|
|
10
|
+
export { pageContextRouter, setPageContextRouterDeps } from "./routers/page-context.js";
|
|
11
|
+
export { profileRouter, setProfileRouterDeps } from "./routers/profile.js";
|
|
12
|
+
export { setSettingsRouterDeps, settingsRouter } from "./routers/settings.js";
|
package/dist/trpc/init.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface TRPCContext {
|
|
|
12
12
|
/** Tenant ID associated with the bearer token, if any. */
|
|
13
13
|
tenantId: string | undefined;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a TRPCContext from an incoming request.
|
|
17
|
+
* Resolves the user from BetterAuth session cookies.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createTRPCContext(req: Request): Promise<TRPCContext>;
|
|
15
20
|
/** Wire the org member repository for tRPC tenant validation. Called from services.ts on startup. */
|
|
16
21
|
export declare function setTrpcOrgMemberRepo(repo: IOrgMemberRepository): void;
|
|
17
22
|
export declare const router: import("@trpc/server").TRPCRouterBuilder<{
|
package/dist/trpc/init.js
CHANGED
|
@@ -8,6 +8,34 @@ import { initTRPC, TRPCError } from "@trpc/server";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { validateTenantAccess } from "../auth/index.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
+
// Context factory — resolves BetterAuth session into TRPCContext
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Create a TRPCContext from an incoming request.
|
|
15
|
+
* Resolves the user from BetterAuth session cookies.
|
|
16
|
+
*/
|
|
17
|
+
export async function createTRPCContext(req) {
|
|
18
|
+
let user;
|
|
19
|
+
let tenantId;
|
|
20
|
+
try {
|
|
21
|
+
const { getAuth } = await import("../auth/better-auth.js");
|
|
22
|
+
const auth = getAuth();
|
|
23
|
+
const session = await auth.api.getSession({ headers: req.headers });
|
|
24
|
+
if (session?.user) {
|
|
25
|
+
const sessionUser = session.user;
|
|
26
|
+
const roles = [];
|
|
27
|
+
if (sessionUser.role)
|
|
28
|
+
roles.push(sessionUser.role);
|
|
29
|
+
user = { id: sessionUser.id, roles };
|
|
30
|
+
tenantId = req.headers.get("x-tenant-id") || sessionUser.id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// No session — unauthenticated request
|
|
35
|
+
}
|
|
36
|
+
return { user, tenantId: tenantId ?? "" };
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
11
39
|
// tRPC init
|
|
12
40
|
// ---------------------------------------------------------------------------
|
|
13
41
|
const t = initTRPC.context().create();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC page-context router — stores and retrieves per-user page context.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import type { IPageContextRepository } from "../../fleet/page-context-repository.js";
|
|
7
|
+
export interface PageContextRouterDeps {
|
|
8
|
+
repo: IPageContextRepository;
|
|
9
|
+
}
|
|
10
|
+
export declare function setPageContextRouterDeps(deps: PageContextRouterDeps): void;
|
|
11
|
+
export declare const pageContextRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
12
|
+
ctx: import("../init.js").TRPCContext;
|
|
13
|
+
meta: object;
|
|
14
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
15
|
+
transformer: false;
|
|
16
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
17
|
+
/** Update the page context for the current user. Called on route change. */
|
|
18
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
19
|
+
input: {
|
|
20
|
+
currentPage: string;
|
|
21
|
+
pagePrompt: string | null;
|
|
22
|
+
};
|
|
23
|
+
output: {
|
|
24
|
+
ok: true;
|
|
25
|
+
};
|
|
26
|
+
meta: object;
|
|
27
|
+
}>;
|
|
28
|
+
/** Get the current page context for the authenticated user. */
|
|
29
|
+
current: import("@trpc/server").TRPCQueryProcedure<{
|
|
30
|
+
input: void;
|
|
31
|
+
output: {
|
|
32
|
+
currentPage: string;
|
|
33
|
+
pagePrompt: string | null;
|
|
34
|
+
} | null;
|
|
35
|
+
meta: object;
|
|
36
|
+
}>;
|
|
37
|
+
}>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC page-context router — stores and retrieves per-user page context.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { protectedProcedure, router } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setPageContextRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Page context not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Schema
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const updatePageContextSchema = z.object({
|
|
22
|
+
currentPage: z.string().min(1).max(500),
|
|
23
|
+
pagePrompt: z.string().max(2000).nullable(),
|
|
24
|
+
});
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Router
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export const pageContextRouter = router({
|
|
29
|
+
/** Update the page context for the current user. Called on route change. */
|
|
30
|
+
update: protectedProcedure.input(updatePageContextSchema).mutation(async ({ ctx, input }) => {
|
|
31
|
+
await deps().repo.set(ctx.user.id, input.currentPage, input.pagePrompt);
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}),
|
|
34
|
+
/** Get the current page context for the authenticated user. */
|
|
35
|
+
current: protectedProcedure.query(async ({ ctx }) => {
|
|
36
|
+
const pc = await deps().repo.get(ctx.user.id);
|
|
37
|
+
if (!pc)
|
|
38
|
+
return null;
|
|
39
|
+
return { currentPage: pc.currentPage, pagePrompt: pc.pagePrompt };
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC profile router — get/update user profile and change password.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProfileRouterDeps {
|
|
7
|
+
getUser: (userId: string) => Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
image: string | null;
|
|
12
|
+
twoFactorEnabled: boolean;
|
|
13
|
+
} | null>;
|
|
14
|
+
updateUser: (userId: string, data: {
|
|
15
|
+
name?: string;
|
|
16
|
+
image?: string | null;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
image: string | null;
|
|
22
|
+
twoFactorEnabled: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
changePassword: (userId: string, currentPassword: string, newPassword: string) => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export declare function setProfileRouterDeps(deps: ProfileRouterDeps): void;
|
|
27
|
+
export declare const profileRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
28
|
+
ctx: import("../init.js").TRPCContext;
|
|
29
|
+
meta: object;
|
|
30
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
31
|
+
transformer: false;
|
|
32
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
33
|
+
/** Get the authenticated user's profile. */
|
|
34
|
+
getProfile: import("@trpc/server").TRPCQueryProcedure<{
|
|
35
|
+
input: void;
|
|
36
|
+
output: {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
email: string;
|
|
40
|
+
image: string | null;
|
|
41
|
+
twoFactorEnabled: boolean;
|
|
42
|
+
};
|
|
43
|
+
meta: object;
|
|
44
|
+
}>;
|
|
45
|
+
/** Update the authenticated user's display name and/or avatar. */
|
|
46
|
+
updateProfile: import("@trpc/server").TRPCMutationProcedure<{
|
|
47
|
+
input: {
|
|
48
|
+
name?: string | undefined;
|
|
49
|
+
image?: string | null | undefined;
|
|
50
|
+
};
|
|
51
|
+
output: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
email: string;
|
|
55
|
+
image: string | null;
|
|
56
|
+
twoFactorEnabled: boolean;
|
|
57
|
+
};
|
|
58
|
+
meta: object;
|
|
59
|
+
}>;
|
|
60
|
+
/** Change the authenticated user's password. */
|
|
61
|
+
changePassword: import("@trpc/server").TRPCMutationProcedure<{
|
|
62
|
+
input: {
|
|
63
|
+
currentPassword: string;
|
|
64
|
+
newPassword: string;
|
|
65
|
+
};
|
|
66
|
+
output: {
|
|
67
|
+
ok: true;
|
|
68
|
+
};
|
|
69
|
+
meta: object;
|
|
70
|
+
}>;
|
|
71
|
+
}>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC profile router — get/update user profile and change password.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — no product-specific imports.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { protectedProcedure, router } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setProfileRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Profile router not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Router
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const profileRouter = router({
|
|
22
|
+
/** Get the authenticated user's profile. */
|
|
23
|
+
getProfile: protectedProcedure.query(async ({ ctx }) => {
|
|
24
|
+
const user = await deps().getUser(ctx.user.id);
|
|
25
|
+
if (!user) {
|
|
26
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id: user.id,
|
|
30
|
+
name: user.name,
|
|
31
|
+
email: user.email,
|
|
32
|
+
image: user.image,
|
|
33
|
+
twoFactorEnabled: user.twoFactorEnabled,
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
/** Update the authenticated user's display name and/or avatar. */
|
|
37
|
+
updateProfile: protectedProcedure
|
|
38
|
+
.input(z.object({
|
|
39
|
+
name: z.string().min(1).max(128).optional(),
|
|
40
|
+
image: z.string().url().max(2048).nullable().optional(),
|
|
41
|
+
}))
|
|
42
|
+
.mutation(async ({ input, ctx }) => {
|
|
43
|
+
const updated = await deps().updateUser(ctx.user.id, {
|
|
44
|
+
...(input.name !== undefined && { name: input.name }),
|
|
45
|
+
...(input.image !== undefined && { image: input.image }),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
id: updated.id,
|
|
49
|
+
name: updated.name,
|
|
50
|
+
email: updated.email,
|
|
51
|
+
image: updated.image,
|
|
52
|
+
twoFactorEnabled: updated.twoFactorEnabled,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
/** Change the authenticated user's password. */
|
|
56
|
+
changePassword: protectedProcedure
|
|
57
|
+
.input(z.object({
|
|
58
|
+
currentPassword: z.string().min(1),
|
|
59
|
+
newPassword: z.string().min(8, "Password must be at least 8 characters"),
|
|
60
|
+
}))
|
|
61
|
+
.mutation(async ({ input, ctx }) => {
|
|
62
|
+
const ok = await deps().changePassword(ctx.user.id, input.currentPassword, input.newPassword);
|
|
63
|
+
if (!ok) {
|
|
64
|
+
throw new TRPCError({ code: "BAD_REQUEST", message: "Current password is incorrect" });
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}),
|
|
68
|
+
});
|