@wopr-network/platform-core 1.69.0 → 1.71.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/db/schema/pool-config.d.ts +41 -0
- package/dist/db/schema/pool-config.js +5 -0
- package/dist/db/schema/pool-instances.d.ts +126 -0
- package/dist/db/schema/pool-instances.js +10 -0
- package/dist/server/__tests__/container.test.js +4 -1
- package/dist/server/container.d.ts +20 -2
- package/dist/server/container.js +20 -5
- package/dist/server/lifecycle.js +10 -0
- package/dist/server/routes/admin.d.ts +18 -0
- package/dist/server/routes/admin.js +21 -0
- package/dist/server/services/hot-pool-claim.d.ts +30 -0
- package/dist/server/services/hot-pool-claim.js +92 -0
- package/dist/server/services/hot-pool.d.ts +25 -0
- package/dist/server/services/hot-pool.js +129 -0
- package/dist/server/services/pool-repository.d.ts +44 -0
- package/dist/server/services/pool-repository.js +72 -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/drizzle/migrations/0025_hot_pool_tables.sql +29 -0
- package/package.json +1 -1
- package/src/db/schema/pool-config.ts +6 -0
- package/src/db/schema/pool-instances.ts +11 -0
- package/src/server/__tests__/container.test.ts +4 -1
- package/src/server/container.ts +38 -8
- package/src/server/lifecycle.ts +10 -0
- package/src/server/routes/admin.ts +26 -0
- package/src/server/services/hot-pool-claim.ts +130 -0
- package/src/server/services/hot-pool.ts +174 -0
- package/src/server/services/pool-repository.ts +107 -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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository for hot pool database operations.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates all pool_config and pool_instances queries behind
|
|
5
|
+
* a testable interface. No raw pool.query() outside this file.
|
|
6
|
+
*/
|
|
7
|
+
import type { Pool } from "pg";
|
|
8
|
+
export interface PoolInstance {
|
|
9
|
+
id: string;
|
|
10
|
+
containerId: string;
|
|
11
|
+
status: string;
|
|
12
|
+
tenantId: string | null;
|
|
13
|
+
name: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface IPoolRepository {
|
|
16
|
+
getPoolSize(): Promise<number>;
|
|
17
|
+
setPoolSize(size: number): Promise<void>;
|
|
18
|
+
warmCount(): Promise<number>;
|
|
19
|
+
insertWarm(id: string, containerId: string): Promise<void>;
|
|
20
|
+
listWarm(): Promise<PoolInstance[]>;
|
|
21
|
+
markDead(id: string): Promise<void>;
|
|
22
|
+
deleteDead(): Promise<void>;
|
|
23
|
+
claimWarm(tenantId: string, name: string): Promise<{
|
|
24
|
+
id: string;
|
|
25
|
+
containerId: string;
|
|
26
|
+
} | null>;
|
|
27
|
+
updateInstanceStatus(id: string, status: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare class DrizzlePoolRepository implements IPoolRepository {
|
|
30
|
+
private pool;
|
|
31
|
+
constructor(pool: Pool);
|
|
32
|
+
getPoolSize(): Promise<number>;
|
|
33
|
+
setPoolSize(size: number): Promise<void>;
|
|
34
|
+
warmCount(): Promise<number>;
|
|
35
|
+
insertWarm(id: string, containerId: string): Promise<void>;
|
|
36
|
+
listWarm(): Promise<PoolInstance[]>;
|
|
37
|
+
markDead(id: string): Promise<void>;
|
|
38
|
+
deleteDead(): Promise<void>;
|
|
39
|
+
claimWarm(tenantId: string, name: string): Promise<{
|
|
40
|
+
id: string;
|
|
41
|
+
containerId: string;
|
|
42
|
+
} | null>;
|
|
43
|
+
updateInstanceStatus(id: string, status: string): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository for hot pool database operations.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates all pool_config and pool_instances queries behind
|
|
5
|
+
* a testable interface. No raw pool.query() outside this file.
|
|
6
|
+
*/
|
|
7
|
+
export class DrizzlePoolRepository {
|
|
8
|
+
pool;
|
|
9
|
+
constructor(pool) {
|
|
10
|
+
this.pool = pool;
|
|
11
|
+
}
|
|
12
|
+
async getPoolSize() {
|
|
13
|
+
try {
|
|
14
|
+
const res = await this.pool.query("SELECT pool_size FROM pool_config WHERE id = 1");
|
|
15
|
+
return res.rows[0]?.pool_size ?? 2;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async setPoolSize(size) {
|
|
22
|
+
await this.pool.query("INSERT INTO pool_config (id, pool_size) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET pool_size = $1", [size]);
|
|
23
|
+
}
|
|
24
|
+
async warmCount() {
|
|
25
|
+
const res = await this.pool.query("SELECT COUNT(*)::int AS count FROM pool_instances WHERE status = 'warm'");
|
|
26
|
+
return res.rows[0].count;
|
|
27
|
+
}
|
|
28
|
+
async insertWarm(id, containerId) {
|
|
29
|
+
await this.pool.query("INSERT INTO pool_instances (id, container_id, status) VALUES ($1, $2, 'warm')", [
|
|
30
|
+
id,
|
|
31
|
+
containerId,
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
async listWarm() {
|
|
35
|
+
const res = await this.pool.query("SELECT id, container_id, status, tenant_id, name FROM pool_instances WHERE status = 'warm'");
|
|
36
|
+
return res.rows.map((r) => ({
|
|
37
|
+
id: r.id,
|
|
38
|
+
containerId: r.container_id,
|
|
39
|
+
status: r.status,
|
|
40
|
+
tenantId: r.tenant_id ?? null,
|
|
41
|
+
name: r.name ?? null,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async markDead(id) {
|
|
45
|
+
await this.pool.query("UPDATE pool_instances SET status = 'dead' WHERE id = $1", [id]);
|
|
46
|
+
}
|
|
47
|
+
async deleteDead() {
|
|
48
|
+
await this.pool.query("DELETE FROM pool_instances WHERE status = 'dead'");
|
|
49
|
+
}
|
|
50
|
+
async claimWarm(tenantId, name) {
|
|
51
|
+
const res = await this.pool.query(`UPDATE pool_instances
|
|
52
|
+
SET status = 'claimed',
|
|
53
|
+
claimed_at = NOW(),
|
|
54
|
+
tenant_id = $1,
|
|
55
|
+
name = $2
|
|
56
|
+
WHERE id = (
|
|
57
|
+
SELECT id FROM pool_instances
|
|
58
|
+
WHERE status = 'warm'
|
|
59
|
+
ORDER BY created_at ASC
|
|
60
|
+
LIMIT 1
|
|
61
|
+
FOR UPDATE SKIP LOCKED
|
|
62
|
+
)
|
|
63
|
+
RETURNING id, container_id`, [tenantId, name]);
|
|
64
|
+
if (res.rowCount === 0)
|
|
65
|
+
return null;
|
|
66
|
+
const row = res.rows[0];
|
|
67
|
+
return { id: row.id, containerId: row.container_id };
|
|
68
|
+
}
|
|
69
|
+
async updateInstanceStatus(id, status) {
|
|
70
|
+
await this.pool.query("UPDATE pool_instances SET status = $1 WHERE id = $2", [status, id]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC settings router — tenant config, preferences, health.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — service name parameterized via deps.
|
|
5
|
+
*/
|
|
6
|
+
import type { INotificationPreferencesRepository } from "../../email/index.js";
|
|
7
|
+
export interface SettingsRouterDeps {
|
|
8
|
+
/** Service name returned by health endpoint (e.g. "paperclip-platform"). */
|
|
9
|
+
serviceName: string;
|
|
10
|
+
getNotificationPrefsStore: () => INotificationPreferencesRepository;
|
|
11
|
+
testProvider?: (provider: string, tenantId: string) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
latencyMs?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function setSettingsRouterDeps(deps: SettingsRouterDeps): void;
|
|
18
|
+
export declare const settingsRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
19
|
+
ctx: import("../init.js").TRPCContext;
|
|
20
|
+
meta: object;
|
|
21
|
+
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
22
|
+
transformer: false;
|
|
23
|
+
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
24
|
+
/** Health check — publicly accessible. */
|
|
25
|
+
health: import("@trpc/server").TRPCQueryProcedure<{
|
|
26
|
+
input: void;
|
|
27
|
+
output: {
|
|
28
|
+
status: "ok";
|
|
29
|
+
service: string;
|
|
30
|
+
};
|
|
31
|
+
meta: object;
|
|
32
|
+
}>;
|
|
33
|
+
/** Get tenant configuration summary. */
|
|
34
|
+
tenantConfig: import("@trpc/server").TRPCQueryProcedure<{
|
|
35
|
+
input: void;
|
|
36
|
+
output: {
|
|
37
|
+
tenantId: string;
|
|
38
|
+
configured: boolean;
|
|
39
|
+
};
|
|
40
|
+
meta: object;
|
|
41
|
+
}>;
|
|
42
|
+
/** Ping — verify auth and tenant context. */
|
|
43
|
+
ping: import("@trpc/server").TRPCQueryProcedure<{
|
|
44
|
+
input: void;
|
|
45
|
+
output: {
|
|
46
|
+
ok: true;
|
|
47
|
+
tenantId: string;
|
|
48
|
+
userId: string;
|
|
49
|
+
timestamp: number;
|
|
50
|
+
};
|
|
51
|
+
meta: object;
|
|
52
|
+
}>;
|
|
53
|
+
/** Get notification preferences for the authenticated tenant. */
|
|
54
|
+
notificationPreferences: import("@trpc/server").TRPCQueryProcedure<{
|
|
55
|
+
input: void;
|
|
56
|
+
output: import("../../email/notification-repository-types.js").NotificationPrefs;
|
|
57
|
+
meta: object;
|
|
58
|
+
}>;
|
|
59
|
+
/** Test connectivity to a provider. */
|
|
60
|
+
testProvider: import("@trpc/server").TRPCMutationProcedure<{
|
|
61
|
+
input: {
|
|
62
|
+
provider: string;
|
|
63
|
+
};
|
|
64
|
+
output: {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
latencyMs?: number;
|
|
67
|
+
error?: string;
|
|
68
|
+
};
|
|
69
|
+
meta: object;
|
|
70
|
+
}>;
|
|
71
|
+
/** Update notification preferences for the authenticated tenant. */
|
|
72
|
+
updateNotificationPreferences: import("@trpc/server").TRPCMutationProcedure<{
|
|
73
|
+
input: {
|
|
74
|
+
billing_low_balance?: boolean | undefined;
|
|
75
|
+
billing_receipts?: boolean | undefined;
|
|
76
|
+
billing_auto_topup?: boolean | undefined;
|
|
77
|
+
agent_channel_disconnect?: boolean | undefined;
|
|
78
|
+
agent_status_changes?: boolean | undefined;
|
|
79
|
+
account_role_changes?: boolean | undefined;
|
|
80
|
+
account_team_invites?: boolean | undefined;
|
|
81
|
+
};
|
|
82
|
+
output: import("../../email/notification-repository-types.js").NotificationPrefs;
|
|
83
|
+
meta: object;
|
|
84
|
+
}>;
|
|
85
|
+
}>>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC settings router — tenant config, preferences, health.
|
|
3
|
+
*
|
|
4
|
+
* Pure platform-core — service name parameterized via deps.
|
|
5
|
+
*/
|
|
6
|
+
import { TRPCError } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { publicProcedure, router, tenantProcedure } from "../init.js";
|
|
9
|
+
let _deps = null;
|
|
10
|
+
export function setSettingsRouterDeps(deps) {
|
|
11
|
+
_deps = deps;
|
|
12
|
+
}
|
|
13
|
+
function deps() {
|
|
14
|
+
if (!_deps)
|
|
15
|
+
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Settings not initialized" });
|
|
16
|
+
return _deps;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Router
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const settingsRouter = router({
|
|
22
|
+
/** Health check — publicly accessible. */
|
|
23
|
+
health: publicProcedure.query(() => {
|
|
24
|
+
return { status: "ok", service: deps().serviceName };
|
|
25
|
+
}),
|
|
26
|
+
/** Get tenant configuration summary. */
|
|
27
|
+
tenantConfig: tenantProcedure.query(({ ctx }) => {
|
|
28
|
+
return {
|
|
29
|
+
tenantId: ctx.tenantId,
|
|
30
|
+
configured: true,
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
/** Ping — verify auth and tenant context. */
|
|
34
|
+
ping: tenantProcedure.query(({ ctx }) => {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
tenantId: ctx.tenantId,
|
|
38
|
+
userId: ctx.user.id,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
42
|
+
/** Get notification preferences for the authenticated tenant. */
|
|
43
|
+
notificationPreferences: tenantProcedure.query(({ ctx }) => {
|
|
44
|
+
const store = deps().getNotificationPrefsStore();
|
|
45
|
+
return store.get(ctx.tenantId);
|
|
46
|
+
}),
|
|
47
|
+
/** Test connectivity to a provider. */
|
|
48
|
+
testProvider: tenantProcedure
|
|
49
|
+
.input(z.object({ provider: z.string().min(1).max(64) }))
|
|
50
|
+
.mutation(async ({ input, ctx }) => {
|
|
51
|
+
const testFn = deps().testProvider;
|
|
52
|
+
if (!testFn) {
|
|
53
|
+
return { ok: false, error: "Provider testing not configured" };
|
|
54
|
+
}
|
|
55
|
+
return testFn(input.provider, ctx.tenantId);
|
|
56
|
+
}),
|
|
57
|
+
/** Update notification preferences for the authenticated tenant. */
|
|
58
|
+
updateNotificationPreferences: tenantProcedure
|
|
59
|
+
.input(z.object({
|
|
60
|
+
billing_low_balance: z.boolean().optional(),
|
|
61
|
+
billing_receipts: z.boolean().optional(),
|
|
62
|
+
billing_auto_topup: z.boolean().optional(),
|
|
63
|
+
agent_channel_disconnect: z.boolean().optional(),
|
|
64
|
+
agent_status_changes: z.boolean().optional(),
|
|
65
|
+
account_role_changes: z.boolean().optional(),
|
|
66
|
+
account_team_invites: z.boolean().optional(),
|
|
67
|
+
}))
|
|
68
|
+
.mutation(async ({ input, ctx }) => {
|
|
69
|
+
const store = deps().getNotificationPrefsStore();
|
|
70
|
+
await store.update(ctx.tenantId, input);
|
|
71
|
+
return store.get(ctx.tenantId);
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Hot pool: pre-provisioned warm containers for instant claiming
|
|
2
|
+
CREATE TABLE IF NOT EXISTS "pool_config" (
|
|
3
|
+
"id" integer PRIMARY KEY DEFAULT 1,
|
|
4
|
+
"pool_size" integer NOT NULL DEFAULT 2
|
|
5
|
+
);
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS "pool_instances" (
|
|
9
|
+
"id" text PRIMARY KEY,
|
|
10
|
+
"container_id" text NOT NULL,
|
|
11
|
+
"status" text NOT NULL DEFAULT 'warm',
|
|
12
|
+
"tenant_id" text,
|
|
13
|
+
"name" text,
|
|
14
|
+
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
15
|
+
"claimed_at" timestamp
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
|
|
19
|
+
-- Claim query: WHERE status = 'warm' ORDER BY created_at ASC FOR UPDATE SKIP LOCKED
|
|
20
|
+
CREATE INDEX IF NOT EXISTS "pool_instances_status_created" ON "pool_instances" ("status", "created_at");
|
|
21
|
+
--> statement-breakpoint
|
|
22
|
+
|
|
23
|
+
-- Tenant lookup for admin queries
|
|
24
|
+
CREATE INDEX IF NOT EXISTS "pool_instances_tenant" ON "pool_instances" ("tenant_id") WHERE "tenant_id" IS NOT NULL;
|
|
25
|
+
--> statement-breakpoint
|
|
26
|
+
|
|
27
|
+
-- Seed default pool config
|
|
28
|
+
INSERT INTO "pool_config" ("id", "pool_size") VALUES (1, 2)
|
|
29
|
+
ON CONFLICT ("id") DO NOTHING;
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const poolInstances = pgTable("pool_instances", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
containerId: text("container_id").notNull(),
|
|
6
|
+
status: text("status").notNull().default("warm"),
|
|
7
|
+
tenantId: text("tenant_id"),
|
|
8
|
+
name: text("name"),
|
|
9
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
10
|
+
claimedAt: timestamp("claimed_at"),
|
|
11
|
+
});
|