@anton.andrusenko/shopify-mcp-admin 2.1.1 → 2.2.1
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/README.md +418 -26
- package/dist/chunk-5QMYOO4B.js +146 -0
- package/dist/chunk-EGGOXEIC.js +249 -0
- package/dist/chunk-JU5IFCVJ.js +208 -0
- package/dist/chunk-LMFNHULG.js +14035 -0
- package/dist/chunk-PQKNBYJN.js +254 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
- package/dist/dashboard/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
- package/dist/dashboard/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/dashboard/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/dashboard/assets/index-BfNrQS4y.js +120 -0
- package/dist/dashboard/assets/index-BfNrQS4y.js.map +1 -0
- package/dist/dashboard/assets/index-HBHxyHsM.css +1 -0
- package/dist/dashboard/index.html +26 -0
- package/dist/dashboard/mcp-icon.svg +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7874 -0
- package/dist/mcp-auth-F25V6FEY.js +24 -0
- package/dist/schema-SOWYIQIV.js +38 -0
- package/dist/security-44M6F2QU.js +112 -0
- package/dist/setup-wizard-PVLOC3DU.js +697 -0
- package/dist/store-JK2ZU6DR.js +10 -0
- package/dist/tools-HVUCP53D.js +82 -0
- package/package.json +4 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// src/config/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var configSchema = z.object({
|
|
4
|
+
// Server operation mode (ADR-008)
|
|
5
|
+
// local: Single-tenant, credentials via env vars (default)
|
|
6
|
+
// remote: Multi-tenant, credentials via database
|
|
7
|
+
SERVER_MODE: z.enum(["local", "remote"]).default("local"),
|
|
8
|
+
// Database connection URL (required in remote mode)
|
|
9
|
+
DATABASE_URL: z.string().url().optional().describe("PostgreSQL connection URL for multi-tenant mode"),
|
|
10
|
+
// Required in local mode - store identity
|
|
11
|
+
SHOPIFY_STORE_URL: z.string().min(1, "SHOPIFY_STORE_URL is required").regex(/\.myshopify\.com$/, "Must be a valid myshopify.com domain").optional(),
|
|
12
|
+
// Authentication Option 1: Legacy Custom App (static token)
|
|
13
|
+
SHOPIFY_ACCESS_TOKEN: z.string().optional(),
|
|
14
|
+
// Authentication Option 2: Dev Dashboard (OAuth 2.0 client credentials)
|
|
15
|
+
SHOPIFY_CLIENT_ID: z.string().optional(),
|
|
16
|
+
SHOPIFY_CLIENT_SECRET: z.string().optional(),
|
|
17
|
+
// Optional with defaults
|
|
18
|
+
SHOPIFY_API_VERSION: z.string().default("2025-10"),
|
|
19
|
+
DEBUG: z.string().optional(),
|
|
20
|
+
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
21
|
+
// Log format configuration (Story 13.1: Structured Logging)
|
|
22
|
+
// json: JSON output for production (default)
|
|
23
|
+
// pretty: Human-readable output for development
|
|
24
|
+
LOG_FORMAT: z.enum(["json", "pretty"]).default("json"),
|
|
25
|
+
PORT: z.string().default("3000").transform(Number),
|
|
26
|
+
// Transport selection (AC-2.2.6, AC-2.2.7)
|
|
27
|
+
// Default: stdio for Claude Desktop compatibility
|
|
28
|
+
TRANSPORT: z.enum(["stdio", "http"]).default("stdio"),
|
|
29
|
+
// Store info cache TTL (milliseconds)
|
|
30
|
+
// Default: 5 minutes (300000ms) - configurable for performance tuning
|
|
31
|
+
STORE_INFO_CACHE_TTL_MS: z.string().optional().default("300000").transform(Number).describe("Cache TTL for store info in milliseconds (default: 5 minutes)"),
|
|
32
|
+
// Lazy loading configuration (Epic 12)
|
|
33
|
+
// Default: false - loads all tools at startup for maximum compatibility
|
|
34
|
+
// Note: Claude Desktop doesn't support dynamic tool refresh via notifications,
|
|
35
|
+
// so lazy loading is disabled by default. Set to 'true' for clients that
|
|
36
|
+
// properly handle notifications/tools/list_changed.
|
|
37
|
+
SHOPIFY_MCP_LAZY_LOADING: z.string().optional().default("false").transform((val) => val === "true" || val === "1").describe("Enable lazy loading of tools via modules (default: false)"),
|
|
38
|
+
// Role preset configuration (Story 12.3: Progressive Loading)
|
|
39
|
+
// Automatically loads appropriate modules at startup based on role
|
|
40
|
+
// Valid roles: inventory-manager, product-manager, content-manager,
|
|
41
|
+
// seo-specialist, international-manager, full-access
|
|
42
|
+
// When not set, only core module is loaded (requires manual module loading)
|
|
43
|
+
SHOPIFY_MCP_ROLE: z.string().optional().describe("Role preset for automatic module loading at startup"),
|
|
44
|
+
// AES-256-GCM encryption key for credential protection (Story 6-2)
|
|
45
|
+
// Must be exactly 64 hex characters (32 bytes = 256 bits)
|
|
46
|
+
// Required in remote mode for encrypting stored Shopify access tokens
|
|
47
|
+
// Generate with: openssl rand -hex 32
|
|
48
|
+
ENCRYPTION_KEY: z.string().regex(
|
|
49
|
+
/^[0-9a-fA-F]{64}$/,
|
|
50
|
+
"ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes). Generate with: openssl rand -hex 32"
|
|
51
|
+
).optional().describe("AES-256-GCM encryption key for credential protection (required in remote mode)"),
|
|
52
|
+
// CORS allowed origins configuration (Story 6-7)
|
|
53
|
+
// Comma-separated list of allowed origins for cross-origin requests
|
|
54
|
+
// Default: '*' (all origins allowed - suitable for development)
|
|
55
|
+
// Example: 'https://app.example.com,https://admin.example.com'
|
|
56
|
+
ALLOWED_ORIGINS: z.string().optional().describe("Comma-separated list of allowed origins for CORS"),
|
|
57
|
+
// Enable HSTS header (Story 6-7)
|
|
58
|
+
// When true, sends Strict-Transport-Security header with HTTPS responses
|
|
59
|
+
// Default: true in remote mode, false in local mode
|
|
60
|
+
ENABLE_HSTS: z.string().optional().default("false").transform((val) => val === "true" || val === "1").describe("Enable HTTP Strict Transport Security header"),
|
|
61
|
+
// Metrics endpoint configuration (Story 13-2: Metrics & Monitoring)
|
|
62
|
+
// Exposes Prometheus-format metrics at /metrics endpoint
|
|
63
|
+
// Default: true in remote mode (HTTP transport), false in local mode (STDIO)
|
|
64
|
+
METRICS_ENDPOINT_ENABLED: z.string().optional().transform((val) => val === "true" || val === "1").describe("Enable Prometheus metrics endpoint at /metrics"),
|
|
65
|
+
// Sentry error tracking DSN (Story 13.7: Production Deployment Infrastructure)
|
|
66
|
+
// Optional but recommended for production error tracking
|
|
67
|
+
// Get from: https://sentry.io → Your Project → Settings → Client Keys (DSN)
|
|
68
|
+
SENTRY_DSN: z.string().url("SENTRY_DSN must be a valid URL").optional().describe("Sentry error tracking DSN (optional but recommended for production)"),
|
|
69
|
+
// Graceful shutdown drain timeout configuration (Story 13-3: Graceful Shutdown)
|
|
70
|
+
// Time in seconds to wait for existing connections to complete before force shutdown
|
|
71
|
+
// Default: 30 seconds, max: 300 seconds (5 minutes)
|
|
72
|
+
// Railway recommends values less than termination grace period (default 10s)
|
|
73
|
+
// Kubernetes default terminationGracePeriodSeconds is 30s
|
|
74
|
+
SHUTDOWN_DRAIN_SECONDS: z.string().optional().default("30").transform(Number).pipe(
|
|
75
|
+
z.number().int("SHUTDOWN_DRAIN_SECONDS must be an integer").positive("SHUTDOWN_DRAIN_SECONDS must be positive").max(300, "SHUTDOWN_DRAIN_SECONDS cannot exceed 300 seconds (5 minutes)")
|
|
76
|
+
).describe("Graceful shutdown drain timeout in seconds (default: 30, max: 300)"),
|
|
77
|
+
// App URL for OAuth redirects (required in remote mode when OAuth is enabled)
|
|
78
|
+
// Used as the redirect_uri in OAuth flows
|
|
79
|
+
APP_URL: z.string().url("APP_URL must be a valid URL").optional().describe("Base URL of the application (required for OAuth redirects in remote mode)")
|
|
80
|
+
}).refine(
|
|
81
|
+
(data) => {
|
|
82
|
+
if (data.SERVER_MODE === "remote" && !data.DATABASE_URL) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
message: "DATABASE_URL is required when SERVER_MODE=remote. Example: postgresql://user:pass@localhost:5432/dbname",
|
|
89
|
+
path: ["DATABASE_URL"]
|
|
90
|
+
}
|
|
91
|
+
).refine(
|
|
92
|
+
(data) => {
|
|
93
|
+
if (data.SERVER_MODE === "remote" && !data.ENCRYPTION_KEY) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
message: "ENCRYPTION_KEY is required when SERVER_MODE=remote. Generate with: openssl rand -hex 32",
|
|
100
|
+
path: ["ENCRYPTION_KEY"]
|
|
101
|
+
}
|
|
102
|
+
).refine(
|
|
103
|
+
(data) => {
|
|
104
|
+
if (data.SERVER_MODE === "local" && !data.SHOPIFY_STORE_URL) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
message: "SHOPIFY_STORE_URL is required when SERVER_MODE=local",
|
|
111
|
+
path: ["SHOPIFY_STORE_URL"]
|
|
112
|
+
}
|
|
113
|
+
).refine(
|
|
114
|
+
(data) => {
|
|
115
|
+
if (data.SERVER_MODE === "remote") {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const hasLegacyAuth = !!data.SHOPIFY_ACCESS_TOKEN;
|
|
119
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
120
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
121
|
+
const hasClientCredentials = hasClientId && hasClientSecret;
|
|
122
|
+
return hasLegacyAuth || hasClientCredentials;
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
message: "Authentication required: Provide either SHOPIFY_ACCESS_TOKEN (legacy) OR both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET (Dev Dashboard)"
|
|
126
|
+
}
|
|
127
|
+
).refine(
|
|
128
|
+
(data) => {
|
|
129
|
+
if (data.SERVER_MODE === "remote") {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
133
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
134
|
+
if (hasClientId && !hasClientSecret) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_SECRET is required when SHOPIFY_CLIENT_ID is provided"
|
|
141
|
+
}
|
|
142
|
+
).refine(
|
|
143
|
+
(data) => {
|
|
144
|
+
if (data.SERVER_MODE === "remote") {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
148
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
149
|
+
if (hasClientSecret && !hasClientId) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_ID is required when SHOPIFY_CLIENT_SECRET is provided"
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
function getAuthMode(config) {
|
|
159
|
+
if (config.SHOPIFY_ACCESS_TOKEN) {
|
|
160
|
+
return "token";
|
|
161
|
+
}
|
|
162
|
+
return "client_credentials";
|
|
163
|
+
}
|
|
164
|
+
function isDebugEnabled(debugValue) {
|
|
165
|
+
if (!debugValue) return false;
|
|
166
|
+
const normalized = debugValue.toLowerCase().trim();
|
|
167
|
+
return normalized === "1" || normalized === "true";
|
|
168
|
+
}
|
|
169
|
+
function isLazyLoadingEnabled(config) {
|
|
170
|
+
return config.SHOPIFY_MCP_LAZY_LOADING;
|
|
171
|
+
}
|
|
172
|
+
function getConfiguredRole(config) {
|
|
173
|
+
return config.SHOPIFY_MCP_ROLE;
|
|
174
|
+
}
|
|
175
|
+
function getServerMode(config) {
|
|
176
|
+
return config.SERVER_MODE;
|
|
177
|
+
}
|
|
178
|
+
function isRemoteMode(config) {
|
|
179
|
+
return config.SERVER_MODE === "remote";
|
|
180
|
+
}
|
|
181
|
+
function getDatabaseUrl(config) {
|
|
182
|
+
return config.DATABASE_URL;
|
|
183
|
+
}
|
|
184
|
+
function getStoreUrl(config) {
|
|
185
|
+
return config.SHOPIFY_STORE_URL;
|
|
186
|
+
}
|
|
187
|
+
function requireStoreUrl(config) {
|
|
188
|
+
if (!config.SHOPIFY_STORE_URL) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"SHOPIFY_STORE_URL is not available in remote mode. Use per-request shop domain instead."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return config.SHOPIFY_STORE_URL;
|
|
194
|
+
}
|
|
195
|
+
function getEncryptionKey(config) {
|
|
196
|
+
if (!config.ENCRYPTION_KEY) {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
return Buffer.from(config.ENCRYPTION_KEY, "hex");
|
|
200
|
+
}
|
|
201
|
+
function requireEncryptionKey(config) {
|
|
202
|
+
if (!config.ENCRYPTION_KEY) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"ENCRYPTION_KEY is required in remote mode. Generate with: openssl rand -hex 32"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return Buffer.from(config.ENCRYPTION_KEY, "hex");
|
|
208
|
+
}
|
|
209
|
+
function getAllowedOrigins(config) {
|
|
210
|
+
if (!config.ALLOWED_ORIGINS) {
|
|
211
|
+
return ["*"];
|
|
212
|
+
}
|
|
213
|
+
return config.ALLOWED_ORIGINS.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
214
|
+
}
|
|
215
|
+
function isHSTSEnabled(config) {
|
|
216
|
+
return config.ENABLE_HSTS;
|
|
217
|
+
}
|
|
218
|
+
function isMetricsEnabled(config) {
|
|
219
|
+
if (config.METRICS_ENDPOINT_ENABLED !== void 0) {
|
|
220
|
+
return config.METRICS_ENDPOINT_ENABLED;
|
|
221
|
+
}
|
|
222
|
+
return config.SERVER_MODE === "remote" && config.TRANSPORT === "http";
|
|
223
|
+
}
|
|
224
|
+
function getShutdownDrainMs(config) {
|
|
225
|
+
return config.SHUTDOWN_DRAIN_SECONDS * 1e3;
|
|
226
|
+
}
|
|
227
|
+
function getShutdownDrainSeconds(config) {
|
|
228
|
+
return config.SHUTDOWN_DRAIN_SECONDS;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
configSchema,
|
|
233
|
+
getAuthMode,
|
|
234
|
+
isDebugEnabled,
|
|
235
|
+
isLazyLoadingEnabled,
|
|
236
|
+
getConfiguredRole,
|
|
237
|
+
getServerMode,
|
|
238
|
+
isRemoteMode,
|
|
239
|
+
getDatabaseUrl,
|
|
240
|
+
getStoreUrl,
|
|
241
|
+
requireStoreUrl,
|
|
242
|
+
getEncryptionKey,
|
|
243
|
+
requireEncryptionKey,
|
|
244
|
+
getAllowedOrigins,
|
|
245
|
+
isHSTSEnabled,
|
|
246
|
+
isMetricsEnabled,
|
|
247
|
+
getShutdownDrainMs,
|
|
248
|
+
getShutdownDrainSeconds
|
|
249
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-5QMYOO4B.js";
|
|
4
|
+
|
|
5
|
+
// src/db/client.ts
|
|
6
|
+
import { PrismaClient } from "@prisma/client";
|
|
7
|
+
import { PrismaClient as PrismaClient2 } from "@prisma/client";
|
|
8
|
+
var prismaInstance = null;
|
|
9
|
+
function getPrismaClient() {
|
|
10
|
+
if (!prismaInstance) {
|
|
11
|
+
log.debug("Creating new Prisma client instance");
|
|
12
|
+
prismaInstance = new PrismaClient({
|
|
13
|
+
log: process.env.DEBUG === "true" ? ["query", "info", "warn", "error"] : ["error"]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return prismaInstance;
|
|
17
|
+
}
|
|
18
|
+
async function disconnectPrisma() {
|
|
19
|
+
if (prismaInstance) {
|
|
20
|
+
log.debug("Disconnecting Prisma client");
|
|
21
|
+
await prismaInstance.$disconnect();
|
|
22
|
+
prismaInstance = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var prisma = getPrismaClient();
|
|
26
|
+
|
|
27
|
+
// src/session/store.ts
|
|
28
|
+
var SessionStore = class {
|
|
29
|
+
cleanupInterval = null;
|
|
30
|
+
prisma = getPrismaClient();
|
|
31
|
+
SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32
|
+
// 24 hours
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve session data by session ID
|
|
35
|
+
* @param sessionId - Unique session identifier
|
|
36
|
+
* @returns SessionData if valid and not expired, null otherwise
|
|
37
|
+
*/
|
|
38
|
+
async get(sessionId) {
|
|
39
|
+
try {
|
|
40
|
+
const session = await this.prisma.tenantSession.findUnique({
|
|
41
|
+
where: { id: sessionId },
|
|
42
|
+
select: {
|
|
43
|
+
tenantId: true,
|
|
44
|
+
expiresAt: true
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (!session) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const expiresAt = session.expiresAt.getTime();
|
|
51
|
+
if (expiresAt < Date.now()) {
|
|
52
|
+
await this.prisma.tenantSession.delete({
|
|
53
|
+
where: { id: sessionId }
|
|
54
|
+
}).catch(() => {
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tenantId: session.tenantId,
|
|
60
|
+
expiresAt
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
log.error("Failed to retrieve session", error instanceof Error ? error : void 0);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Store session data with automatic 24-hour expiration
|
|
69
|
+
* @param sessionId - Unique session identifier
|
|
70
|
+
* @param data - Session data (tenantId)
|
|
71
|
+
*/
|
|
72
|
+
async set(sessionId, data) {
|
|
73
|
+
const expiresAt = new Date(Date.now() + this.SESSION_TTL_MS);
|
|
74
|
+
try {
|
|
75
|
+
await this.prisma.tenantSession.upsert({
|
|
76
|
+
where: { id: sessionId },
|
|
77
|
+
update: {
|
|
78
|
+
expiresAt
|
|
79
|
+
},
|
|
80
|
+
create: {
|
|
81
|
+
id: sessionId,
|
|
82
|
+
tenantId: data.tenantId,
|
|
83
|
+
expiresAt
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.error("Failed to store session", error instanceof Error ? error : void 0);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delete a specific session
|
|
93
|
+
* @param sessionId - Session identifier to delete
|
|
94
|
+
*/
|
|
95
|
+
async delete(sessionId) {
|
|
96
|
+
try {
|
|
97
|
+
await this.prisma.tenantSession.delete({
|
|
98
|
+
where: { id: sessionId }
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error && typeof error === "object" && "code" in error && error.code !== "P2025") {
|
|
102
|
+
log.error("Failed to delete session", error instanceof Error ? error : void 0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Delete all sessions for a specific tenant
|
|
108
|
+
* Used when a tenant changes their password for security
|
|
109
|
+
* @param tenantId - Tenant identifier
|
|
110
|
+
*/
|
|
111
|
+
async deleteByTenantId(tenantId) {
|
|
112
|
+
try {
|
|
113
|
+
await this.prisma.tenantSession.deleteMany({
|
|
114
|
+
where: { tenantId }
|
|
115
|
+
});
|
|
116
|
+
log.debug(`Deleted all sessions for tenant ${tenantId}`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
log.error("Failed to delete tenant sessions", error instanceof Error ? error : void 0);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove all expired sessions from the store
|
|
124
|
+
*/
|
|
125
|
+
async cleanup() {
|
|
126
|
+
try {
|
|
127
|
+
const result = await this.prisma.tenantSession.deleteMany({
|
|
128
|
+
where: {
|
|
129
|
+
expiresAt: {
|
|
130
|
+
lt: /* @__PURE__ */ new Date()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (result.count > 0) {
|
|
135
|
+
log.debug(`Cleaned up ${result.count} expired sessions`);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
log.error("Session cleanup failed", error instanceof Error ? error : void 0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Start automatic cleanup task (runs every 10 minutes)
|
|
143
|
+
* @returns Timer handle for cleanup task
|
|
144
|
+
*/
|
|
145
|
+
startCleanup() {
|
|
146
|
+
if (this.cleanupInterval) {
|
|
147
|
+
return this.cleanupInterval;
|
|
148
|
+
}
|
|
149
|
+
this.cleanupInterval = setInterval(
|
|
150
|
+
() => {
|
|
151
|
+
this.cleanup().catch((error) => {
|
|
152
|
+
log.error("Background session cleanup error", error instanceof Error ? error : void 0);
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
10 * 60 * 1e3
|
|
156
|
+
);
|
|
157
|
+
this.cleanupInterval.unref();
|
|
158
|
+
log.info("Session cleanup background task started (runs every 10 minutes)");
|
|
159
|
+
return this.cleanupInterval;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Stop the automatic cleanup task
|
|
163
|
+
*/
|
|
164
|
+
stopCleanup() {
|
|
165
|
+
if (this.cleanupInterval) {
|
|
166
|
+
clearInterval(this.cleanupInterval);
|
|
167
|
+
this.cleanupInterval = null;
|
|
168
|
+
log.info("Session cleanup background task stopped");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the number of active sessions (for testing/monitoring)
|
|
173
|
+
*/
|
|
174
|
+
async size() {
|
|
175
|
+
try {
|
|
176
|
+
return await this.prisma.tenantSession.count({
|
|
177
|
+
where: {
|
|
178
|
+
expiresAt: {
|
|
179
|
+
gte: /* @__PURE__ */ new Date()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
log.error("Failed to count sessions", error instanceof Error ? error : void 0);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Clear all sessions (for testing)
|
|
190
|
+
*/
|
|
191
|
+
async clear() {
|
|
192
|
+
try {
|
|
193
|
+
await this.prisma.tenantSession.deleteMany();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log.error("Failed to clear sessions", error instanceof Error ? error : void 0);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var sessionStore = new SessionStore();
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
getPrismaClient,
|
|
204
|
+
disconnectPrisma,
|
|
205
|
+
prisma,
|
|
206
|
+
SessionStore,
|
|
207
|
+
sessionStore
|
|
208
|
+
};
|