@wopr-network/platform-core 1.11.0 → 1.12.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.
@@ -7,11 +7,79 @@ export interface IDeletionExecutorRepository {
7
7
  markCompleted(id: string, deletionSummary: string): Promise<boolean>;
8
8
  /** Find the active (pending) deletion request for a tenant, if any. */
9
9
  findPendingByTenant(tenantId: string): Promise<DeletionRequestRow | null>;
10
+ deleteBotInstances(tenantId: string): Promise<number>;
11
+ deleteCreditTransactions(tenantId: string): Promise<number>;
12
+ deleteCreditBalances(tenantId: string): Promise<number>;
13
+ deleteCreditAdjustments(tenantId: string): Promise<number | null>;
14
+ deleteMeterEvents(tenantId: string): Promise<number>;
15
+ deleteUsageSummaries(tenantId: string): Promise<number>;
16
+ deleteBillingPeriodSummaries(tenantId: string): Promise<number>;
17
+ deleteStripeUsageReports(tenantId: string): Promise<number>;
18
+ deleteNotificationQueue(tenantId: string): Promise<number>;
19
+ deleteNotificationPreferences(tenantId: string): Promise<number>;
20
+ deleteEmailNotifications(tenantId: string): Promise<number>;
21
+ deleteAuditLog(tenantId: string): Promise<number>;
22
+ anonymizeAuditLog(tenantId: string): Promise<number>;
23
+ deleteAdminNotes(tenantId: string): Promise<number>;
24
+ listSnapshotS3Keys(tenantId: string): Promise<{
25
+ id: string;
26
+ s3Key: string | null;
27
+ }[]>;
28
+ deleteSnapshots(tenantId: string): Promise<number>;
29
+ deleteBackupStatus(tenantId: string): Promise<number>;
30
+ deletePayramCharges(tenantId: string): Promise<number>;
31
+ deleteTenantStatus(tenantId: string): Promise<number>;
32
+ deleteUserRolesByUser(tenantId: string): Promise<number>;
33
+ deleteUserRolesByTenant(tenantId: string): Promise<number>;
34
+ deleteTenantCustomers(tenantId: string): Promise<number>;
35
+ deleteAuthUser(tenantId: string): Promise<{
36
+ sessionChanges: number;
37
+ accountChanges: number;
38
+ verificationChanges: number;
39
+ userChanges: number;
40
+ }>;
10
41
  }
11
42
  export declare class DrizzleDeletionExecutorRepository implements IDeletionExecutorRepository {
12
43
  private readonly db;
13
- constructor(db: PlatformDb);
44
+ private readonly authDb?;
45
+ constructor(db: PlatformDb, authDb?: {
46
+ query: (sql: string, params?: unknown[]) => Promise<{
47
+ affectedRows?: number;
48
+ rowCount?: number;
49
+ }>;
50
+ } | undefined);
14
51
  findRipe(now: string): Promise<DeletionRequestRow[]>;
15
52
  markCompleted(id: string, deletionSummary: string): Promise<boolean>;
16
53
  findPendingByTenant(tenantId: string): Promise<DeletionRequestRow | null>;
54
+ deleteBotInstances(tenantId: string): Promise<number>;
55
+ deleteCreditTransactions(tenantId: string): Promise<number>;
56
+ deleteCreditBalances(tenantId: string): Promise<number>;
57
+ deleteCreditAdjustments(tenantId: string): Promise<number | null>;
58
+ deleteMeterEvents(tenantId: string): Promise<number>;
59
+ deleteUsageSummaries(tenantId: string): Promise<number>;
60
+ deleteBillingPeriodSummaries(tenantId: string): Promise<number>;
61
+ deleteStripeUsageReports(tenantId: string): Promise<number>;
62
+ deleteNotificationQueue(tenantId: string): Promise<number>;
63
+ deleteNotificationPreferences(tenantId: string): Promise<number>;
64
+ deleteEmailNotifications(tenantId: string): Promise<number>;
65
+ deleteAuditLog(tenantId: string): Promise<number>;
66
+ anonymizeAuditLog(tenantId: string): Promise<number>;
67
+ deleteAdminNotes(tenantId: string): Promise<number>;
68
+ listSnapshotS3Keys(tenantId: string): Promise<{
69
+ id: string;
70
+ s3Key: string | null;
71
+ }[]>;
72
+ deleteSnapshots(tenantId: string): Promise<number>;
73
+ deleteBackupStatus(tenantId: string): Promise<number>;
74
+ deletePayramCharges(tenantId: string): Promise<number>;
75
+ deleteTenantStatus(tenantId: string): Promise<number>;
76
+ deleteUserRolesByUser(tenantId: string): Promise<number>;
77
+ deleteUserRolesByTenant(tenantId: string): Promise<number>;
78
+ deleteTenantCustomers(tenantId: string): Promise<number>;
79
+ deleteAuthUser(tenantId: string): Promise<{
80
+ sessionChanges: number;
81
+ accountChanges: number;
82
+ verificationChanges: number;
83
+ userChanges: number;
84
+ }>;
17
85
  }
@@ -1,13 +1,15 @@
1
- import { and, eq, lte, sql } from "drizzle-orm";
2
- import { accountDeletionRequests } from "../db/schema/index.js";
1
+ import { and, eq, like, lte, or, sql } from "drizzle-orm";
2
+ import { accountDeletionRequests, adminAuditLog, adminNotes, auditLog, backupStatus, billingPeriodSummaries, botInstances, creditBalances, creditTransactions, emailNotifications, meterEvents, notificationPreferences, notificationQueue, payramCharges, snapshots, stripeUsageReports, tenantCustomers, tenantStatus, usageSummaries, userRoles, } from "../db/schema/index.js";
3
3
  import { toRow } from "./deletion-repository.js";
4
4
  // ---------------------------------------------------------------------------
5
5
  // Implementation
6
6
  // ---------------------------------------------------------------------------
7
7
  export class DrizzleDeletionExecutorRepository {
8
8
  db;
9
- constructor(db) {
9
+ authDb;
10
+ constructor(db, authDb) {
10
11
  this.db = db;
12
+ this.authDb = authDb;
11
13
  }
12
14
  async findRipe(now) {
13
15
  const rows = await this.db
@@ -38,4 +40,194 @@ export class DrizzleDeletionExecutorRepository {
38
40
  const row = rows[0];
39
41
  return row ? toRow(row) : null;
40
42
  }
43
+ // --- Data deletion methods ---
44
+ async deleteBotInstances(tenantId) {
45
+ const result = await this.db
46
+ .delete(botInstances)
47
+ .where(eq(botInstances.tenantId, tenantId))
48
+ .returning({ id: botInstances.id });
49
+ return result.length;
50
+ }
51
+ async deleteCreditTransactions(tenantId) {
52
+ const result = await this.db
53
+ .delete(creditTransactions)
54
+ .where(eq(creditTransactions.tenantId, tenantId))
55
+ .returning({ id: creditTransactions.id });
56
+ return result.length;
57
+ }
58
+ async deleteCreditBalances(tenantId) {
59
+ const result = await this.db
60
+ .delete(creditBalances)
61
+ .where(eq(creditBalances.tenantId, tenantId))
62
+ .returning({ tenantId: creditBalances.tenantId });
63
+ return result.length;
64
+ }
65
+ async deleteCreditAdjustments(tenantId) {
66
+ // raw SQL: credit_adjustments is not in the Drizzle schema (optional table)
67
+ try {
68
+ const result = await this.db.execute(sql `DELETE FROM credit_adjustments WHERE tenant_id = ${tenantId}`);
69
+ return result.rowCount ?? 0;
70
+ }
71
+ catch (err) {
72
+ const pgCode = err.code;
73
+ if (pgCode === "42P01")
74
+ return null; // table does not exist
75
+ throw err;
76
+ }
77
+ }
78
+ async deleteMeterEvents(tenantId) {
79
+ const result = await this.db
80
+ .delete(meterEvents)
81
+ .where(eq(meterEvents.tenant, tenantId))
82
+ .returning({ id: meterEvents.id });
83
+ return result.length;
84
+ }
85
+ async deleteUsageSummaries(tenantId) {
86
+ const result = await this.db
87
+ .delete(usageSummaries)
88
+ .where(eq(usageSummaries.tenant, tenantId))
89
+ .returning({ id: usageSummaries.id });
90
+ return result.length;
91
+ }
92
+ async deleteBillingPeriodSummaries(tenantId) {
93
+ const result = await this.db
94
+ .delete(billingPeriodSummaries)
95
+ .where(eq(billingPeriodSummaries.tenant, tenantId))
96
+ .returning({ id: billingPeriodSummaries.id });
97
+ return result.length;
98
+ }
99
+ async deleteStripeUsageReports(tenantId) {
100
+ const result = await this.db
101
+ .delete(stripeUsageReports)
102
+ .where(eq(stripeUsageReports.tenant, tenantId))
103
+ .returning({ id: stripeUsageReports.id });
104
+ return result.length;
105
+ }
106
+ async deleteNotificationQueue(tenantId) {
107
+ const result = await this.db
108
+ .delete(notificationQueue)
109
+ .where(eq(notificationQueue.tenantId, tenantId))
110
+ .returning({ id: notificationQueue.id });
111
+ return result.length;
112
+ }
113
+ async deleteNotificationPreferences(tenantId) {
114
+ const result = await this.db
115
+ .delete(notificationPreferences)
116
+ .where(eq(notificationPreferences.tenantId, tenantId))
117
+ .returning({ tenantId: notificationPreferences.tenantId });
118
+ return result.length;
119
+ }
120
+ async deleteEmailNotifications(tenantId) {
121
+ const result = await this.db
122
+ .delete(emailNotifications)
123
+ .where(eq(emailNotifications.tenantId, tenantId))
124
+ .returning({ id: emailNotifications.id });
125
+ return result.length;
126
+ }
127
+ async deleteAuditLog(tenantId) {
128
+ const result = await this.db.delete(auditLog).where(eq(auditLog.userId, tenantId)).returning({ id: auditLog.id });
129
+ return result.length;
130
+ }
131
+ async anonymizeAuditLog(tenantId) {
132
+ const result = await this.db
133
+ .update(adminAuditLog)
134
+ .set({ targetTenant: "[deleted]", targetUser: "[deleted]" })
135
+ .where(or(eq(adminAuditLog.targetTenant, tenantId), eq(adminAuditLog.targetUser, tenantId)))
136
+ .returning({ id: adminAuditLog.id });
137
+ return result.length;
138
+ }
139
+ async deleteAdminNotes(tenantId) {
140
+ const result = await this.db
141
+ .delete(adminNotes)
142
+ .where(eq(adminNotes.tenantId, tenantId))
143
+ .returning({ id: adminNotes.id });
144
+ return result.length;
145
+ }
146
+ async listSnapshotS3Keys(tenantId) {
147
+ const rows = await this.db
148
+ .select({ id: snapshots.id, s3Key: snapshots.s3Key })
149
+ .from(snapshots)
150
+ .where(eq(snapshots.tenant, tenantId));
151
+ return rows;
152
+ }
153
+ async deleteSnapshots(tenantId) {
154
+ const result = await this.db
155
+ .delete(snapshots)
156
+ .where(eq(snapshots.tenant, tenantId))
157
+ .returning({ id: snapshots.id });
158
+ return result.length;
159
+ }
160
+ async deleteBackupStatus(tenantId) {
161
+ // Escape LIKE wildcards in tenantId to prevent injection
162
+ const safeTenantId = tenantId.replace(/%/g, "\\%").replace(/_/g, "\\_");
163
+ // backup_status uses containerId with pattern "tenant_{id}_..."
164
+ const result = await this.db
165
+ .delete(backupStatus)
166
+ .where(like(backupStatus.containerId, `tenant_${safeTenantId}%`))
167
+ .returning({ containerId: backupStatus.containerId });
168
+ return result.length;
169
+ }
170
+ async deletePayramCharges(tenantId) {
171
+ const result = await this.db
172
+ .delete(payramCharges)
173
+ .where(eq(payramCharges.tenantId, tenantId))
174
+ .returning({ referenceId: payramCharges.referenceId });
175
+ return result.length;
176
+ }
177
+ async deleteTenantStatus(tenantId) {
178
+ const result = await this.db
179
+ .delete(tenantStatus)
180
+ .where(eq(tenantStatus.tenantId, tenantId))
181
+ .returning({ tenantId: tenantStatus.tenantId });
182
+ return result.length;
183
+ }
184
+ async deleteUserRolesByUser(tenantId) {
185
+ const result = await this.db
186
+ .delete(userRoles)
187
+ .where(eq(userRoles.userId, tenantId))
188
+ .returning({ userId: userRoles.userId });
189
+ return result.length;
190
+ }
191
+ async deleteUserRolesByTenant(tenantId) {
192
+ const result = await this.db
193
+ .delete(userRoles)
194
+ .where(eq(userRoles.tenantId, tenantId))
195
+ .returning({ userId: userRoles.userId });
196
+ return result.length;
197
+ }
198
+ async deleteTenantCustomers(tenantId) {
199
+ const result = await this.db
200
+ .delete(tenantCustomers)
201
+ .where(eq(tenantCustomers.tenant, tenantId))
202
+ .returning({ tenant: tenantCustomers.tenant });
203
+ return result.length;
204
+ }
205
+ async deleteAuthUser(tenantId) {
206
+ if (!this.authDb) {
207
+ return { sessionChanges: 0, accountChanges: 0, verificationChanges: 0, userChanges: 0 };
208
+ }
209
+ const getCount = (result) => result.affectedRows ?? result.rowCount ?? 0;
210
+ // raw SQL: better-auth tables are not in the Drizzle schema
211
+ // Wrapped in a transaction — all four DELETEs must succeed or none.
212
+ await this.authDb.query("BEGIN", []);
213
+ try {
214
+ const sessionResult = await this.authDb.query(`DELETE FROM session WHERE user_id = $1`, [tenantId]);
215
+ const accountResult = await this.authDb.query(`DELETE FROM account WHERE user_id = $1`, [tenantId]);
216
+ const verificationResult = await this.authDb.query(`DELETE FROM email_verification_tokens WHERE user_id = $1`, [
217
+ tenantId,
218
+ ]);
219
+ const userResult = await this.authDb.query(`DELETE FROM "user" WHERE id = $1`, [tenantId]);
220
+ await this.authDb.query("COMMIT", []);
221
+ return {
222
+ sessionChanges: getCount(sessionResult),
223
+ accountChanges: getCount(accountResult),
224
+ verificationChanges: getCount(verificationResult),
225
+ userChanges: getCount(userResult),
226
+ };
227
+ }
228
+ catch (err) {
229
+ await this.authDb.query("ROLLBACK", []);
230
+ throw err;
231
+ }
232
+ }
41
233
  }
@@ -1,12 +1,23 @@
1
1
  import type { PlatformDb } from "../db/index.js";
2
2
  import { accountDeletionRequests } from "../db/schema/index.js";
3
- import type { DeletionRequestRow, InsertDeletionRequest } from "./repository-types.js";
3
+ import type { DeletionRequestRow, DeletionStatus, InsertDeletionRequest } from "./repository-types.js";
4
4
  export type { DeletionRequestRow, InsertDeletionRequest };
5
5
  export interface IDeletionRepository {
6
6
  insert(data: InsertDeletionRequest): Promise<void>;
7
7
  getById(id: string): Promise<DeletionRequestRow | null>;
8
8
  listByTenant(tenantId: string): Promise<DeletionRequestRow[]>;
9
+ getPendingForTenant(tenantId: string): Promise<DeletionRequestRow | null>;
9
10
  cancel(id: string, cancelReason: string): Promise<boolean>;
11
+ markCompleted(id: string, summary: string): Promise<void>;
12
+ findExpired(): Promise<DeletionRequestRow[]>;
13
+ list(opts: {
14
+ status?: DeletionStatus;
15
+ limit: number;
16
+ offset: number;
17
+ }): Promise<{
18
+ requests: DeletionRequestRow[];
19
+ total: number;
20
+ }>;
10
21
  }
11
22
  export declare class DrizzleDeletionRepository implements IDeletionRepository {
12
23
  private readonly db;
@@ -14,6 +25,17 @@ export declare class DrizzleDeletionRepository implements IDeletionRepository {
14
25
  insert(data: InsertDeletionRequest): Promise<void>;
15
26
  getById(id: string): Promise<DeletionRequestRow | null>;
16
27
  listByTenant(tenantId: string): Promise<DeletionRequestRow[]>;
28
+ getPendingForTenant(tenantId: string): Promise<DeletionRequestRow | null>;
17
29
  cancel(id: string, cancelReason: string): Promise<boolean>;
30
+ markCompleted(id: string, summary: string): Promise<void>;
31
+ findExpired(): Promise<DeletionRequestRow[]>;
32
+ list(opts: {
33
+ status?: DeletionStatus;
34
+ limit: number;
35
+ offset: number;
36
+ }): Promise<{
37
+ requests: DeletionRequestRow[];
38
+ total: number;
39
+ }>;
18
40
  }
19
41
  export declare function toRow(row: typeof accountDeletionRequests.$inferSelect): DeletionRequestRow;
@@ -1,4 +1,4 @@
1
- import { and, eq, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, lte, sql } from "drizzle-orm";
2
2
  import { accountDeletionRequests } from "../db/schema/index.js";
3
3
  // ---------------------------------------------------------------------------
4
4
  // Implementation
@@ -9,11 +9,12 @@ export class DrizzleDeletionRepository {
9
9
  this.db = db;
10
10
  }
11
11
  async insert(data) {
12
+ const deleteAfter = data.deleteAfter ?? new Date(Date.now() + (data.graceDays ?? 30) * 24 * 60 * 60 * 1000).toISOString();
12
13
  await this.db.insert(accountDeletionRequests).values({
13
14
  id: data.id,
14
15
  tenantId: data.tenantId,
15
16
  requestedBy: data.requestedBy,
16
- deleteAfter: data.deleteAfter,
17
+ deleteAfter,
17
18
  reason: data.reason ?? null,
18
19
  });
19
20
  }
@@ -29,6 +30,15 @@ export class DrizzleDeletionRepository {
29
30
  .where(eq(accountDeletionRequests.tenantId, tenantId));
30
31
  return rows.map(toRow);
31
32
  }
33
+ async getPendingForTenant(tenantId) {
34
+ const rows = await this.db
35
+ .select()
36
+ .from(accountDeletionRequests)
37
+ .where(and(eq(accountDeletionRequests.tenantId, tenantId), eq(accountDeletionRequests.status, "pending")))
38
+ .limit(1);
39
+ const row = rows[0];
40
+ return row ? toRow(row) : null;
41
+ }
32
42
  async cancel(id, cancelReason) {
33
43
  const result = await this.db
34
44
  .update(accountDeletionRequests)
@@ -41,6 +51,41 @@ export class DrizzleDeletionRepository {
41
51
  .returning({ id: accountDeletionRequests.id });
42
52
  return result.length > 0;
43
53
  }
54
+ async markCompleted(id, summary) {
55
+ await this.db
56
+ .update(accountDeletionRequests)
57
+ .set({
58
+ status: "completed",
59
+ completedAt: sql `now()`,
60
+ deletionSummary: summary,
61
+ updatedAt: sql `now()`,
62
+ })
63
+ .where(and(eq(accountDeletionRequests.id, id), eq(accountDeletionRequests.status, "pending")));
64
+ }
65
+ async findExpired() {
66
+ const rows = await this.db
67
+ .select()
68
+ .from(accountDeletionRequests)
69
+ .where(and(eq(accountDeletionRequests.status, "pending"), lte(accountDeletionRequests.deleteAfter, sql `now()`)));
70
+ return rows.map(toRow);
71
+ }
72
+ async list(opts) {
73
+ const conditions = opts.status ? eq(accountDeletionRequests.status, opts.status) : undefined;
74
+ const [rows, totalResult] = await Promise.all([
75
+ this.db
76
+ .select()
77
+ .from(accountDeletionRequests)
78
+ .where(conditions)
79
+ .orderBy(desc(accountDeletionRequests.createdAt))
80
+ .limit(opts.limit)
81
+ .offset(opts.offset),
82
+ this.db.select({ count: count() }).from(accountDeletionRequests).where(conditions),
83
+ ]);
84
+ return {
85
+ requests: rows.map(toRow),
86
+ total: totalResult[0]?.count ?? 0,
87
+ };
88
+ }
44
89
  }
45
90
  // ---------------------------------------------------------------------------
46
91
  // Row mapper
@@ -1,5 +1,5 @@
1
1
  import type { PlatformDb } from "../db/index.js";
2
- import type { ExportRequestRow, InsertExportRequest } from "./repository-types.js";
2
+ import type { ExportRequestRow, ExportStatus, InsertExportRequest } from "./repository-types.js";
3
3
  export type { ExportRequestRow, InsertExportRequest };
4
4
  export interface IExportRepository {
5
5
  insert(data: InsertExportRequest): Promise<void>;
@@ -8,6 +8,15 @@ export interface IExportRepository {
8
8
  markProcessing(id: string): Promise<boolean>;
9
9
  markCompleted(id: string, downloadUrl: string): Promise<boolean>;
10
10
  markFailed(id: string, errorMessage?: string): Promise<boolean>;
11
+ list(filters: {
12
+ status?: ExportStatus;
13
+ limit: number;
14
+ offset: number;
15
+ }): Promise<{
16
+ rows: ExportRequestRow[];
17
+ total: number;
18
+ }>;
19
+ updateStatus(id: string, status: ExportStatus, downloadUrl?: string): Promise<void>;
11
20
  }
12
21
  export declare class DrizzleExportRepository implements IExportRepository {
13
22
  private readonly db;
@@ -18,4 +27,13 @@ export declare class DrizzleExportRepository implements IExportRepository {
18
27
  markProcessing(id: string): Promise<boolean>;
19
28
  markCompleted(id: string, downloadUrl: string): Promise<boolean>;
20
29
  markFailed(id: string, _errorMessage?: string): Promise<boolean>;
30
+ list(filters: {
31
+ status?: ExportStatus;
32
+ limit: number;
33
+ offset: number;
34
+ }): Promise<{
35
+ rows: ExportRequestRow[];
36
+ total: number;
37
+ }>;
38
+ updateStatus(id: string, status: ExportStatus, downloadUrl?: string): Promise<void>;
21
39
  }
@@ -1,4 +1,4 @@
1
- import { and, eq, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, sql } from "drizzle-orm";
2
2
  import { accountExportRequests } from "../db/schema/index.js";
3
3
  // ---------------------------------------------------------------------------
4
4
  // Implementation
@@ -59,6 +59,34 @@ export class DrizzleExportRepository {
59
59
  .returning({ id: accountExportRequests.id });
60
60
  return result.length > 0;
61
61
  }
62
+ async list(filters) {
63
+ const conditions = filters.status ? eq(accountExportRequests.status, filters.status) : undefined;
64
+ const [rows, totalResult] = await Promise.all([
65
+ this.db
66
+ .select()
67
+ .from(accountExportRequests)
68
+ .where(conditions)
69
+ .orderBy(desc(accountExportRequests.createdAt))
70
+ .limit(filters.limit)
71
+ .offset(filters.offset),
72
+ this.db.select({ count: count() }).from(accountExportRequests).where(conditions),
73
+ ]);
74
+ return {
75
+ rows: rows.map(toRow),
76
+ total: totalResult[0]?.count ?? 0,
77
+ };
78
+ }
79
+ async updateStatus(id, status, downloadUrl) {
80
+ await this.db
81
+ .update(accountExportRequests)
82
+ .set({
83
+ status,
84
+ // Clear stale downloadUrl when transitioning away from completed
85
+ downloadUrl: downloadUrl ?? (status === "completed" ? undefined : null),
86
+ updatedAt: sql `now()`,
87
+ })
88
+ .where(eq(accountExportRequests.id, id));
89
+ }
62
90
  }
63
91
  // ---------------------------------------------------------------------------
64
92
  // Row mapper
@@ -17,8 +17,11 @@ export interface InsertDeletionRequest {
17
17
  id: string;
18
18
  tenantId: string;
19
19
  requestedBy: string;
20
- deleteAfter: string;
21
- reason?: string;
20
+ /** Explicit ISO timestamp for when deletion should execute. */
21
+ deleteAfter?: string;
22
+ /** Number of grace days from now. Used if deleteAfter is not provided. */
23
+ graceDays?: number;
24
+ reason?: string | null;
22
25
  }
23
26
  export interface ExportRequestRow {
24
27
  id: string;
@@ -15,6 +15,8 @@ import type { Context, Next } from "hono";
15
15
  import type { IApiKeyRepository } from "./api-key-repository.js";
16
16
  import type { Auth } from "./better-auth.js";
17
17
  import type { AuthUser } from "./index.js";
18
+ /** Minimum length for API key tokens (rejects obviously short tokens). */
19
+ export declare const MIN_API_KEY_LENGTH = 22;
18
20
  export interface SessionAuthEnv {
19
21
  Variables: {
20
22
  user: AuthUser;
@@ -12,6 +12,8 @@
12
12
  * If neither is present, the request is rejected with 401.
13
13
  */
14
14
  import { createHash } from "node:crypto";
15
+ /** Minimum length for API key tokens (rejects obviously short tokens). */
16
+ export const MIN_API_KEY_LENGTH = 22;
15
17
  /**
16
18
  * Create middleware that authenticates requests via better-auth session cookies.
17
19
  *
@@ -15,6 +15,9 @@ 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
17
  export { schema };
18
+ export type { SQL } from "drizzle-orm";
19
+ export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
20
+ export { pgTable, text } from "drizzle-orm/pg-core";
18
21
  export type { AuthUser, IAuthUserRepository } from "./auth-user-repository.js";
19
22
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
20
23
  export { creditColumn } from "./credit-column.js";
package/dist/db/index.js CHANGED
@@ -5,5 +5,10 @@ export function createDb(pool) {
5
5
  return drizzle(pool, { schema });
6
6
  }
7
7
  export { schema };
8
+ // Re-export commonly used drizzle-orm operators so consumers using pnpm link
9
+ // resolve them from the same drizzle-orm instance as the schema tables.
10
+ export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
11
+ // Re-export pg-core table builders for consumers that define local tables.
12
+ export { pgTable, text } from "drizzle-orm/pg-core";
8
13
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
9
14
  export { creditColumn } from "./credit-column.js";
@@ -97,7 +97,7 @@ export class AdapterSocket {
97
97
  const charge = adapterResult.charge ?? withMargin(adapterResult.cost, margin);
98
98
  // Emit meter event — BYOK tenants get zero cost/charge (WOP-512)
99
99
  const isByok = request.byok === true;
100
- this.meter.emit({
100
+ await this.meter.emit({
101
101
  tenant: request.tenantId,
102
102
  cost: isByok ? Credit.ZERO : adapterResult.cost,
103
103
  charge: isByok ? Credit.ZERO : charge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,27 @@
1
- import { and, eq, lte, sql } from "drizzle-orm";
1
+ import { and, eq, like, lte, or, sql } from "drizzle-orm";
2
2
  import type { PlatformDb } from "../db/index.js";
3
- import { accountDeletionRequests } from "../db/schema/index.js";
3
+ import {
4
+ accountDeletionRequests,
5
+ adminAuditLog,
6
+ adminNotes,
7
+ auditLog,
8
+ backupStatus,
9
+ billingPeriodSummaries,
10
+ botInstances,
11
+ creditBalances,
12
+ creditTransactions,
13
+ emailNotifications,
14
+ meterEvents,
15
+ notificationPreferences,
16
+ notificationQueue,
17
+ payramCharges,
18
+ snapshots,
19
+ stripeUsageReports,
20
+ tenantCustomers,
21
+ tenantStatus,
22
+ usageSummaries,
23
+ userRoles,
24
+ } from "../db/schema/index.js";
4
25
  import { toRow } from "./deletion-repository.js";
5
26
  import type { DeletionRequestRow } from "./repository-types.js";
6
27
 
@@ -15,6 +36,36 @@ export interface IDeletionExecutorRepository {
15
36
  markCompleted(id: string, deletionSummary: string): Promise<boolean>;
16
37
  /** Find the active (pending) deletion request for a tenant, if any. */
17
38
  findPendingByTenant(tenantId: string): Promise<DeletionRequestRow | null>;
39
+
40
+ // --- Data deletion methods (GDPR purge) ---
41
+ deleteBotInstances(tenantId: string): Promise<number>;
42
+ deleteCreditTransactions(tenantId: string): Promise<number>;
43
+ deleteCreditBalances(tenantId: string): Promise<number>;
44
+ deleteCreditAdjustments(tenantId: string): Promise<number | null>;
45
+ deleteMeterEvents(tenantId: string): Promise<number>;
46
+ deleteUsageSummaries(tenantId: string): Promise<number>;
47
+ deleteBillingPeriodSummaries(tenantId: string): Promise<number>;
48
+ deleteStripeUsageReports(tenantId: string): Promise<number>;
49
+ deleteNotificationQueue(tenantId: string): Promise<number>;
50
+ deleteNotificationPreferences(tenantId: string): Promise<number>;
51
+ deleteEmailNotifications(tenantId: string): Promise<number>;
52
+ deleteAuditLog(tenantId: string): Promise<number>;
53
+ anonymizeAuditLog(tenantId: string): Promise<number>;
54
+ deleteAdminNotes(tenantId: string): Promise<number>;
55
+ listSnapshotS3Keys(tenantId: string): Promise<{ id: string; s3Key: string | null }[]>;
56
+ deleteSnapshots(tenantId: string): Promise<number>;
57
+ deleteBackupStatus(tenantId: string): Promise<number>;
58
+ deletePayramCharges(tenantId: string): Promise<number>;
59
+ deleteTenantStatus(tenantId: string): Promise<number>;
60
+ deleteUserRolesByUser(tenantId: string): Promise<number>;
61
+ deleteUserRolesByTenant(tenantId: string): Promise<number>;
62
+ deleteTenantCustomers(tenantId: string): Promise<number>;
63
+ deleteAuthUser(tenantId: string): Promise<{
64
+ sessionChanges: number;
65
+ accountChanges: number;
66
+ verificationChanges: number;
67
+ userChanges: number;
68
+ }>;
18
69
  }
19
70
 
20
71
  // ---------------------------------------------------------------------------
@@ -22,7 +73,12 @@ export interface IDeletionExecutorRepository {
22
73
  // ---------------------------------------------------------------------------
23
74
 
24
75
  export class DrizzleDeletionExecutorRepository implements IDeletionExecutorRepository {
25
- constructor(private readonly db: PlatformDb) {}
76
+ constructor(
77
+ private readonly db: PlatformDb,
78
+ private readonly authDb?: {
79
+ query: (sql: string, params?: unknown[]) => Promise<{ affectedRows?: number; rowCount?: number }>;
80
+ },
81
+ ) {}
26
82
 
27
83
  async findRipe(now: string): Promise<DeletionRequestRow[]> {
28
84
  const rows = await this.db
@@ -55,4 +111,224 @@ export class DrizzleDeletionExecutorRepository implements IDeletionExecutorRepos
55
111
  const row = rows[0];
56
112
  return row ? toRow(row) : null;
57
113
  }
114
+
115
+ // --- Data deletion methods ---
116
+
117
+ async deleteBotInstances(tenantId: string): Promise<number> {
118
+ const result = await this.db
119
+ .delete(botInstances)
120
+ .where(eq(botInstances.tenantId, tenantId))
121
+ .returning({ id: botInstances.id });
122
+ return result.length;
123
+ }
124
+
125
+ async deleteCreditTransactions(tenantId: string): Promise<number> {
126
+ const result = await this.db
127
+ .delete(creditTransactions)
128
+ .where(eq(creditTransactions.tenantId, tenantId))
129
+ .returning({ id: creditTransactions.id });
130
+ return result.length;
131
+ }
132
+
133
+ async deleteCreditBalances(tenantId: string): Promise<number> {
134
+ const result = await this.db
135
+ .delete(creditBalances)
136
+ .where(eq(creditBalances.tenantId, tenantId))
137
+ .returning({ tenantId: creditBalances.tenantId });
138
+ return result.length;
139
+ }
140
+
141
+ async deleteCreditAdjustments(tenantId: string): Promise<number | null> {
142
+ // raw SQL: credit_adjustments is not in the Drizzle schema (optional table)
143
+ try {
144
+ const result = await this.db.execute(sql`DELETE FROM credit_adjustments WHERE tenant_id = ${tenantId}`);
145
+ return (result as unknown as { rowCount?: number }).rowCount ?? 0;
146
+ } catch (err: unknown) {
147
+ const pgCode = (err as { code?: string }).code;
148
+ if (pgCode === "42P01") return null; // table does not exist
149
+ throw err;
150
+ }
151
+ }
152
+
153
+ async deleteMeterEvents(tenantId: string): Promise<number> {
154
+ const result = await this.db
155
+ .delete(meterEvents)
156
+ .where(eq(meterEvents.tenant, tenantId))
157
+ .returning({ id: meterEvents.id });
158
+ return result.length;
159
+ }
160
+
161
+ async deleteUsageSummaries(tenantId: string): Promise<number> {
162
+ const result = await this.db
163
+ .delete(usageSummaries)
164
+ .where(eq(usageSummaries.tenant, tenantId))
165
+ .returning({ id: usageSummaries.id });
166
+ return result.length;
167
+ }
168
+
169
+ async deleteBillingPeriodSummaries(tenantId: string): Promise<number> {
170
+ const result = await this.db
171
+ .delete(billingPeriodSummaries)
172
+ .where(eq(billingPeriodSummaries.tenant, tenantId))
173
+ .returning({ id: billingPeriodSummaries.id });
174
+ return result.length;
175
+ }
176
+
177
+ async deleteStripeUsageReports(tenantId: string): Promise<number> {
178
+ const result = await this.db
179
+ .delete(stripeUsageReports)
180
+ .where(eq(stripeUsageReports.tenant, tenantId))
181
+ .returning({ id: stripeUsageReports.id });
182
+ return result.length;
183
+ }
184
+
185
+ async deleteNotificationQueue(tenantId: string): Promise<number> {
186
+ const result = await this.db
187
+ .delete(notificationQueue)
188
+ .where(eq(notificationQueue.tenantId, tenantId))
189
+ .returning({ id: notificationQueue.id });
190
+ return result.length;
191
+ }
192
+
193
+ async deleteNotificationPreferences(tenantId: string): Promise<number> {
194
+ const result = await this.db
195
+ .delete(notificationPreferences)
196
+ .where(eq(notificationPreferences.tenantId, tenantId))
197
+ .returning({ tenantId: notificationPreferences.tenantId });
198
+ return result.length;
199
+ }
200
+
201
+ async deleteEmailNotifications(tenantId: string): Promise<number> {
202
+ const result = await this.db
203
+ .delete(emailNotifications)
204
+ .where(eq(emailNotifications.tenantId, tenantId))
205
+ .returning({ id: emailNotifications.id });
206
+ return result.length;
207
+ }
208
+
209
+ async deleteAuditLog(tenantId: string): Promise<number> {
210
+ const result = await this.db.delete(auditLog).where(eq(auditLog.userId, tenantId)).returning({ id: auditLog.id });
211
+ return result.length;
212
+ }
213
+
214
+ async anonymizeAuditLog(tenantId: string): Promise<number> {
215
+ const result = await this.db
216
+ .update(adminAuditLog)
217
+ .set({ targetTenant: "[deleted]", targetUser: "[deleted]" })
218
+ .where(or(eq(adminAuditLog.targetTenant, tenantId), eq(adminAuditLog.targetUser, tenantId)))
219
+ .returning({ id: adminAuditLog.id });
220
+ return result.length;
221
+ }
222
+
223
+ async deleteAdminNotes(tenantId: string): Promise<number> {
224
+ const result = await this.db
225
+ .delete(adminNotes)
226
+ .where(eq(adminNotes.tenantId, tenantId))
227
+ .returning({ id: adminNotes.id });
228
+ return result.length;
229
+ }
230
+
231
+ async listSnapshotS3Keys(tenantId: string): Promise<{ id: string; s3Key: string | null }[]> {
232
+ const rows = await this.db
233
+ .select({ id: snapshots.id, s3Key: snapshots.s3Key })
234
+ .from(snapshots)
235
+ .where(eq(snapshots.tenant, tenantId));
236
+ return rows;
237
+ }
238
+
239
+ async deleteSnapshots(tenantId: string): Promise<number> {
240
+ const result = await this.db
241
+ .delete(snapshots)
242
+ .where(eq(snapshots.tenant, tenantId))
243
+ .returning({ id: snapshots.id });
244
+ return result.length;
245
+ }
246
+
247
+ async deleteBackupStatus(tenantId: string): Promise<number> {
248
+ // Escape LIKE wildcards in tenantId to prevent injection
249
+ const safeTenantId = tenantId.replace(/%/g, "\\%").replace(/_/g, "\\_");
250
+ // backup_status uses containerId with pattern "tenant_{id}_..."
251
+ const result = await this.db
252
+ .delete(backupStatus)
253
+ .where(like(backupStatus.containerId, `tenant_${safeTenantId}%`))
254
+ .returning({ containerId: backupStatus.containerId });
255
+ return result.length;
256
+ }
257
+
258
+ async deletePayramCharges(tenantId: string): Promise<number> {
259
+ const result = await this.db
260
+ .delete(payramCharges)
261
+ .where(eq(payramCharges.tenantId, tenantId))
262
+ .returning({ referenceId: payramCharges.referenceId });
263
+ return result.length;
264
+ }
265
+
266
+ async deleteTenantStatus(tenantId: string): Promise<number> {
267
+ const result = await this.db
268
+ .delete(tenantStatus)
269
+ .where(eq(tenantStatus.tenantId, tenantId))
270
+ .returning({ tenantId: tenantStatus.tenantId });
271
+ return result.length;
272
+ }
273
+
274
+ async deleteUserRolesByUser(tenantId: string): Promise<number> {
275
+ const result = await this.db
276
+ .delete(userRoles)
277
+ .where(eq(userRoles.userId, tenantId))
278
+ .returning({ userId: userRoles.userId });
279
+ return result.length;
280
+ }
281
+
282
+ async deleteUserRolesByTenant(tenantId: string): Promise<number> {
283
+ const result = await this.db
284
+ .delete(userRoles)
285
+ .where(eq(userRoles.tenantId, tenantId))
286
+ .returning({ userId: userRoles.userId });
287
+ return result.length;
288
+ }
289
+
290
+ async deleteTenantCustomers(tenantId: string): Promise<number> {
291
+ const result = await this.db
292
+ .delete(tenantCustomers)
293
+ .where(eq(tenantCustomers.tenant, tenantId))
294
+ .returning({ tenant: tenantCustomers.tenant });
295
+ return result.length;
296
+ }
297
+
298
+ async deleteAuthUser(tenantId: string): Promise<{
299
+ sessionChanges: number;
300
+ accountChanges: number;
301
+ verificationChanges: number;
302
+ userChanges: number;
303
+ }> {
304
+ if (!this.authDb) {
305
+ return { sessionChanges: 0, accountChanges: 0, verificationChanges: 0, userChanges: 0 };
306
+ }
307
+
308
+ const getCount = (result: { affectedRows?: number; rowCount?: number }): number =>
309
+ result.affectedRows ?? result.rowCount ?? 0;
310
+
311
+ // raw SQL: better-auth tables are not in the Drizzle schema
312
+ // Wrapped in a transaction — all four DELETEs must succeed or none.
313
+ await this.authDb.query("BEGIN", []);
314
+ try {
315
+ const sessionResult = await this.authDb.query(`DELETE FROM session WHERE user_id = $1`, [tenantId]);
316
+ const accountResult = await this.authDb.query(`DELETE FROM account WHERE user_id = $1`, [tenantId]);
317
+ const verificationResult = await this.authDb.query(`DELETE FROM email_verification_tokens WHERE user_id = $1`, [
318
+ tenantId,
319
+ ]);
320
+ const userResult = await this.authDb.query(`DELETE FROM "user" WHERE id = $1`, [tenantId]);
321
+ await this.authDb.query("COMMIT", []);
322
+
323
+ return {
324
+ sessionChanges: getCount(sessionResult),
325
+ accountChanges: getCount(accountResult),
326
+ verificationChanges: getCount(verificationResult),
327
+ userChanges: getCount(userResult),
328
+ };
329
+ } catch (err) {
330
+ await this.authDb.query("ROLLBACK", []);
331
+ throw err;
332
+ }
333
+ }
58
334
  }
@@ -1,7 +1,7 @@
1
- import { and, eq, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, lte, sql } from "drizzle-orm";
2
2
  import type { PlatformDb } from "../db/index.js";
3
3
  import { accountDeletionRequests } from "../db/schema/index.js";
4
- import type { DeletionRequestRow, InsertDeletionRequest } from "./repository-types.js";
4
+ import type { DeletionRequestRow, DeletionStatus, InsertDeletionRequest } from "./repository-types.js";
5
5
 
6
6
  export type { DeletionRequestRow, InsertDeletionRequest };
7
7
 
@@ -13,7 +13,15 @@ export interface IDeletionRepository {
13
13
  insert(data: InsertDeletionRequest): Promise<void>;
14
14
  getById(id: string): Promise<DeletionRequestRow | null>;
15
15
  listByTenant(tenantId: string): Promise<DeletionRequestRow[]>;
16
+ getPendingForTenant(tenantId: string): Promise<DeletionRequestRow | null>;
16
17
  cancel(id: string, cancelReason: string): Promise<boolean>;
18
+ markCompleted(id: string, summary: string): Promise<void>;
19
+ findExpired(): Promise<DeletionRequestRow[]>;
20
+ list(opts: {
21
+ status?: DeletionStatus;
22
+ limit: number;
23
+ offset: number;
24
+ }): Promise<{ requests: DeletionRequestRow[]; total: number }>;
17
25
  }
18
26
 
19
27
  // ---------------------------------------------------------------------------
@@ -24,11 +32,13 @@ export class DrizzleDeletionRepository implements IDeletionRepository {
24
32
  constructor(private readonly db: PlatformDb) {}
25
33
 
26
34
  async insert(data: InsertDeletionRequest): Promise<void> {
35
+ const deleteAfter =
36
+ data.deleteAfter ?? new Date(Date.now() + (data.graceDays ?? 30) * 24 * 60 * 60 * 1000).toISOString();
27
37
  await this.db.insert(accountDeletionRequests).values({
28
38
  id: data.id,
29
39
  tenantId: data.tenantId,
30
40
  requestedBy: data.requestedBy,
31
- deleteAfter: data.deleteAfter,
41
+ deleteAfter,
32
42
  reason: data.reason ?? null,
33
43
  });
34
44
  }
@@ -47,6 +57,16 @@ export class DrizzleDeletionRepository implements IDeletionRepository {
47
57
  return rows.map(toRow);
48
58
  }
49
59
 
60
+ async getPendingForTenant(tenantId: string): Promise<DeletionRequestRow | null> {
61
+ const rows = await this.db
62
+ .select()
63
+ .from(accountDeletionRequests)
64
+ .where(and(eq(accountDeletionRequests.tenantId, tenantId), eq(accountDeletionRequests.status, "pending")))
65
+ .limit(1);
66
+ const row = rows[0];
67
+ return row ? toRow(row) : null;
68
+ }
69
+
50
70
  async cancel(id: string, cancelReason: string): Promise<boolean> {
51
71
  const result = await this.db
52
72
  .update(accountDeletionRequests)
@@ -59,6 +79,50 @@ export class DrizzleDeletionRepository implements IDeletionRepository {
59
79
  .returning({ id: accountDeletionRequests.id });
60
80
  return result.length > 0;
61
81
  }
82
+
83
+ async markCompleted(id: string, summary: string): Promise<void> {
84
+ await this.db
85
+ .update(accountDeletionRequests)
86
+ .set({
87
+ status: "completed",
88
+ completedAt: sql`now()`,
89
+ deletionSummary: summary,
90
+ updatedAt: sql`now()`,
91
+ })
92
+ .where(and(eq(accountDeletionRequests.id, id), eq(accountDeletionRequests.status, "pending")));
93
+ }
94
+
95
+ async findExpired(): Promise<DeletionRequestRow[]> {
96
+ const rows = await this.db
97
+ .select()
98
+ .from(accountDeletionRequests)
99
+ .where(and(eq(accountDeletionRequests.status, "pending"), lte(accountDeletionRequests.deleteAfter, sql`now()`)));
100
+ return rows.map(toRow);
101
+ }
102
+
103
+ async list(opts: {
104
+ status?: DeletionStatus;
105
+ limit: number;
106
+ offset: number;
107
+ }): Promise<{ requests: DeletionRequestRow[]; total: number }> {
108
+ const conditions = opts.status ? eq(accountDeletionRequests.status, opts.status) : undefined;
109
+
110
+ const [rows, totalResult] = await Promise.all([
111
+ this.db
112
+ .select()
113
+ .from(accountDeletionRequests)
114
+ .where(conditions)
115
+ .orderBy(desc(accountDeletionRequests.createdAt))
116
+ .limit(opts.limit)
117
+ .offset(opts.offset),
118
+ this.db.select({ count: count() }).from(accountDeletionRequests).where(conditions),
119
+ ]);
120
+
121
+ return {
122
+ requests: rows.map(toRow),
123
+ total: totalResult[0]?.count ?? 0,
124
+ };
125
+ }
62
126
  }
63
127
 
64
128
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
- import { and, eq, sql } from "drizzle-orm";
1
+ import { and, count, desc, eq, sql } from "drizzle-orm";
2
2
  import type { PlatformDb } from "../db/index.js";
3
3
  import { accountExportRequests } from "../db/schema/index.js";
4
- import type { ExportRequestRow, InsertExportRequest } from "./repository-types.js";
4
+ import type { ExportRequestRow, ExportStatus, InsertExportRequest } from "./repository-types.js";
5
5
 
6
6
  export type { ExportRequestRow, InsertExportRequest };
7
7
 
@@ -16,6 +16,12 @@ export interface IExportRepository {
16
16
  markProcessing(id: string): Promise<boolean>;
17
17
  markCompleted(id: string, downloadUrl: string): Promise<boolean>;
18
18
  markFailed(id: string, errorMessage?: string): Promise<boolean>;
19
+ list(filters: {
20
+ status?: ExportStatus;
21
+ limit: number;
22
+ offset: number;
23
+ }): Promise<{ rows: ExportRequestRow[]; total: number }>;
24
+ updateStatus(id: string, status: ExportStatus, downloadUrl?: string): Promise<void>;
19
25
  }
20
26
 
21
27
  // ---------------------------------------------------------------------------
@@ -81,6 +87,42 @@ export class DrizzleExportRepository implements IExportRepository {
81
87
  .returning({ id: accountExportRequests.id });
82
88
  return result.length > 0;
83
89
  }
90
+
91
+ async list(filters: {
92
+ status?: ExportStatus;
93
+ limit: number;
94
+ offset: number;
95
+ }): Promise<{ rows: ExportRequestRow[]; total: number }> {
96
+ const conditions = filters.status ? eq(accountExportRequests.status, filters.status) : undefined;
97
+
98
+ const [rows, totalResult] = await Promise.all([
99
+ this.db
100
+ .select()
101
+ .from(accountExportRequests)
102
+ .where(conditions)
103
+ .orderBy(desc(accountExportRequests.createdAt))
104
+ .limit(filters.limit)
105
+ .offset(filters.offset),
106
+ this.db.select({ count: count() }).from(accountExportRequests).where(conditions),
107
+ ]);
108
+
109
+ return {
110
+ rows: rows.map(toRow),
111
+ total: totalResult[0]?.count ?? 0,
112
+ };
113
+ }
114
+
115
+ async updateStatus(id: string, status: ExportStatus, downloadUrl?: string): Promise<void> {
116
+ await this.db
117
+ .update(accountExportRequests)
118
+ .set({
119
+ status,
120
+ // Clear stale downloadUrl when transitioning away from completed
121
+ downloadUrl: downloadUrl ?? (status === "completed" ? undefined : null),
122
+ updatedAt: sql`now()`,
123
+ })
124
+ .where(eq(accountExportRequests.id, id));
125
+ }
84
126
  }
85
127
 
86
128
  // ---------------------------------------------------------------------------
@@ -23,8 +23,11 @@ export interface InsertDeletionRequest {
23
23
  id: string;
24
24
  tenantId: string;
25
25
  requestedBy: string;
26
- deleteAfter: string;
27
- reason?: string;
26
+ /** Explicit ISO timestamp for when deletion should execute. */
27
+ deleteAfter?: string;
28
+ /** Number of grace days from now. Used if deleteAfter is not provided. */
29
+ graceDays?: number;
30
+ reason?: string | null;
28
31
  }
29
32
 
30
33
  export interface ExportRequestRow {
@@ -18,6 +18,9 @@ import type { IApiKeyRepository } from "./api-key-repository.js";
18
18
  import type { Auth } from "./better-auth.js";
19
19
  import type { AuthUser } from "./index.js";
20
20
 
21
+ /** Minimum length for API key tokens (rejects obviously short tokens). */
22
+ export const MIN_API_KEY_LENGTH = 22;
23
+
21
24
  export interface SessionAuthEnv {
22
25
  Variables: {
23
26
  user: AuthUser;
package/src/db/index.ts CHANGED
@@ -24,6 +24,13 @@ export function createDb(pool: Pool): PlatformDb {
24
24
  }
25
25
 
26
26
  export { schema };
27
+
28
+ export type { SQL } from "drizzle-orm";
29
+ // Re-export commonly used drizzle-orm operators so consumers using pnpm link
30
+ // resolve them from the same drizzle-orm instance as the schema tables.
31
+ export { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
32
+ // Re-export pg-core table builders for consumers that define local tables.
33
+ export { pgTable, text } from "drizzle-orm/pg-core";
27
34
  export type { AuthUser, IAuthUserRepository } from "./auth-user-repository.js";
28
35
  export { BetterAuthUserRepository } from "./auth-user-repository.js";
29
36
  export { creditColumn } from "./credit-column.js";
@@ -147,7 +147,7 @@ export class AdapterSocket {
147
147
 
148
148
  // Emit meter event — BYOK tenants get zero cost/charge (WOP-512)
149
149
  const isByok = request.byok === true;
150
- this.meter.emit({
150
+ await this.meter.emit({
151
151
  tenant: request.tenantId,
152
152
  cost: isByok ? Credit.ZERO : adapterResult.cost,
153
153
  charge: isByok ? Credit.ZERO : charge,