@anton.andrusenko/shopify-mcp-admin 2.1.1 → 2.1.2
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 +18 -8
- package/dist/chunk-DCQTHXKI.js +124 -0
- package/dist/chunk-EGGOXEIC.js +249 -0
- package/dist/chunk-QXLLD2A7.js +130 -0
- package/dist/chunk-UXI33LQD.js +208 -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-BSh6M640.js +107 -0
- package/dist/dashboard/assets/index-BSh6M640.js.map +1 -0
- package/dist/dashboard/assets/index-DLoVESj2.css +1 -0
- package/dist/dashboard/index.html +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18446 -0
- package/dist/mcp-auth-2WVQELV5.js +16 -0
- package/dist/schema-SOWYIQIV.js +38 -0
- package/dist/security-XP6MXK5B.js +98 -0
- package/dist/setup-wizard-PVLOC3DU.js +697 -0
- package/dist/store-FM7HCQVW.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1249,17 +1249,27 @@ All checks must pass before merging pull requests.
|
|
|
1249
1249
|
To publish a new version to npm:
|
|
1250
1250
|
|
|
1251
1251
|
```bash
|
|
1252
|
-
# 1.
|
|
1253
|
-
npm version
|
|
1254
|
-
|
|
1255
|
-
# 2.
|
|
1256
|
-
git
|
|
1252
|
+
# 1. Bump version (does NOT create a tag)
|
|
1253
|
+
npm run version:patch # or version:minor, or version:major
|
|
1254
|
+
|
|
1255
|
+
# 2. Commit + push the version bump to main (this triggers CI + Railway auto-deploy)
|
|
1256
|
+
git add package.json package-lock.json
|
|
1257
|
+
git commit -m "Release v$(node -p \"require('./package.json').version\")"
|
|
1258
|
+
git push origin main
|
|
1259
|
+
|
|
1260
|
+
# 3. Create and push the version tag (this triggers the Release workflow)
|
|
1261
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
1262
|
+
git tag "v$VERSION"
|
|
1263
|
+
git push origin "v$VERSION"
|
|
1257
1264
|
```
|
|
1258
1265
|
|
|
1259
1266
|
The release workflow automatically:
|
|
1260
|
-
1.
|
|
1261
|
-
2.
|
|
1262
|
-
3.
|
|
1267
|
+
1. Reads release notes from `CHANGELOG.md`
|
|
1268
|
+
2. Builds the package (including dashboard assets)
|
|
1269
|
+
3. Publishes to npm
|
|
1270
|
+
4. Creates a GitHub Release
|
|
1271
|
+
|
|
1272
|
+
**Important:** The Release workflow triggers on tags (`v*`) and does **not** rerun the full CI matrix. Tag releases only after the `CI` workflow is green for the commit you’re tagging.
|
|
1263
1273
|
|
|
1264
1274
|
### Repository Setup (for Maintainers)
|
|
1265
1275
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-QXLLD2A7.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/mcp-auth.ts
|
|
6
|
+
var MCP_AUTH_ERROR_CODES = {
|
|
7
|
+
AUTH_REQUIRED: -32001,
|
|
8
|
+
// Missing authentication
|
|
9
|
+
AUTH_INVALID: -32002,
|
|
10
|
+
// Invalid API key format or key
|
|
11
|
+
AUTH_REVOKED: -32003,
|
|
12
|
+
// API key has been revoked
|
|
13
|
+
AUTH_FORBIDDEN: -32004
|
|
14
|
+
// Tenant suspended or no access
|
|
15
|
+
};
|
|
16
|
+
function createJsonRpcError(code, message, hint) {
|
|
17
|
+
const error = {
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
error: {
|
|
20
|
+
code,
|
|
21
|
+
message
|
|
22
|
+
},
|
|
23
|
+
id: null
|
|
24
|
+
};
|
|
25
|
+
if (hint) {
|
|
26
|
+
error.error.data = { hint };
|
|
27
|
+
}
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
function parseBearerToken(authHeader) {
|
|
31
|
+
if (!authHeader) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parts = authHeader.split(" ");
|
|
35
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const token = parts[1];
|
|
39
|
+
if (!token || token.trim() === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
async function validateMcpApiKey(authHeader, apiKeyService, prisma) {
|
|
45
|
+
const token = parseBearerToken(authHeader);
|
|
46
|
+
if (!token) {
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
error: "Authentication required",
|
|
50
|
+
httpStatus: 401
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const validationResult = await apiKeyService.validate(token);
|
|
54
|
+
if (!validationResult.valid) {
|
|
55
|
+
const isRevoked = validationResult.error?.includes("revoked");
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: validationResult.error || "Invalid API key",
|
|
59
|
+
httpStatus: isRevoked ? 403 : 401
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const tenant = validationResult.tenant;
|
|
63
|
+
const tenantShops = await prisma.tenantShop.findMany({
|
|
64
|
+
where: {
|
|
65
|
+
tenantId: tenant.tenantId,
|
|
66
|
+
uninstalledAt: null
|
|
67
|
+
// Only active shops
|
|
68
|
+
},
|
|
69
|
+
select: {
|
|
70
|
+
shopDomain: true,
|
|
71
|
+
scopes: true
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const allowedShops = tenantShops.map((shop) => shop.shopDomain);
|
|
75
|
+
const mcpTenantContext = {
|
|
76
|
+
tenantId: tenant.tenantId,
|
|
77
|
+
email: tenant.email,
|
|
78
|
+
defaultShop: validationResult.shop,
|
|
79
|
+
allowedShops
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
valid: true,
|
|
83
|
+
tenant: mcpTenantContext
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createMcpAuthMiddleware(options) {
|
|
87
|
+
const { apiKeyService, prisma, isRemote } = options;
|
|
88
|
+
return async (req, res, next) => {
|
|
89
|
+
if (!isRemote) {
|
|
90
|
+
log.debug("[mcp-auth] Local mode: skipping API key validation");
|
|
91
|
+
next();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const authResult = await validateMcpApiKey(req.headers.authorization, apiKeyService, prisma);
|
|
95
|
+
if (!authResult.valid) {
|
|
96
|
+
log.debug(`[mcp-auth] Auth failed: ${authResult.error}`);
|
|
97
|
+
let errorCode;
|
|
98
|
+
let hint;
|
|
99
|
+
if (authResult.httpStatus === 401) {
|
|
100
|
+
errorCode = authResult.error === "Authentication required" ? MCP_AUTH_ERROR_CODES.AUTH_REQUIRED : MCP_AUTH_ERROR_CODES.AUTH_INVALID;
|
|
101
|
+
hint = "Include Authorization: Bearer sk_live_xxx header";
|
|
102
|
+
} else {
|
|
103
|
+
errorCode = authResult.error?.includes("revoked") ? MCP_AUTH_ERROR_CODES.AUTH_REVOKED : MCP_AUTH_ERROR_CODES.AUTH_FORBIDDEN;
|
|
104
|
+
hint = "API key has been revoked or tenant is inactive";
|
|
105
|
+
}
|
|
106
|
+
res.status(authResult.httpStatus || 401).json(createJsonRpcError(errorCode, authResult.error || "Authentication failed", hint));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
req.mcpTenantContext = authResult.tenant;
|
|
110
|
+
log.debug(
|
|
111
|
+
`[mcp-auth] Auth successful for tenant: ${authResult.tenant?.tenantId?.substring(0, 8)}...`
|
|
112
|
+
);
|
|
113
|
+
next();
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
var MCP_AUTH_ERRORS = MCP_AUTH_ERROR_CODES;
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
createJsonRpcError,
|
|
120
|
+
parseBearerToken,
|
|
121
|
+
validateMcpApiKey,
|
|
122
|
+
createMcpAuthMiddleware,
|
|
123
|
+
MCP_AUTH_ERRORS
|
|
124
|
+
};
|
|
@@ -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,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configSchema,
|
|
3
|
+
isDebugEnabled
|
|
4
|
+
} from "./chunk-EGGOXEIC.js";
|
|
5
|
+
|
|
6
|
+
// src/config/index.ts
|
|
7
|
+
var _config = null;
|
|
8
|
+
function getConfig() {
|
|
9
|
+
if (_config !== null) {
|
|
10
|
+
return _config;
|
|
11
|
+
}
|
|
12
|
+
const result = configSchema.safeParse(process.env);
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
const errors = result.error.errors.map((err) => {
|
|
15
|
+
const path = err.path.join(".");
|
|
16
|
+
return ` - ${path}: ${err.message}`;
|
|
17
|
+
});
|
|
18
|
+
console.error("Configuration error:");
|
|
19
|
+
console.error(errors.join("\n"));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
_config = result.data;
|
|
23
|
+
return _config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/utils/logger.ts
|
|
27
|
+
var SANITIZATION_PATTERNS = [
|
|
28
|
+
{ pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
29
|
+
{ pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
30
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
|
|
31
|
+
{ pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
|
|
32
|
+
{ pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" }
|
|
33
|
+
];
|
|
34
|
+
function sanitizeLogMessage(message) {
|
|
35
|
+
let result = message;
|
|
36
|
+
for (const { pattern, replacement } of SANITIZATION_PATTERNS) {
|
|
37
|
+
result = result.replace(pattern, replacement);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
42
|
+
if (typeof obj === "string") {
|
|
43
|
+
return sanitizeLogMessage(obj);
|
|
44
|
+
}
|
|
45
|
+
if (obj === null || typeof obj !== "object") {
|
|
46
|
+
return obj;
|
|
47
|
+
}
|
|
48
|
+
if (seen.has(obj)) {
|
|
49
|
+
return "[Circular]";
|
|
50
|
+
}
|
|
51
|
+
seen.add(obj);
|
|
52
|
+
if (Array.isArray(obj)) {
|
|
53
|
+
return obj.map((item) => sanitizeObject(item, seen));
|
|
54
|
+
}
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
57
|
+
result[key] = sanitizeObject(value, seen);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function safeStringify(data) {
|
|
62
|
+
try {
|
|
63
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
64
|
+
return JSON.stringify(data, (_key, value) => {
|
|
65
|
+
if (typeof value === "object" && value !== null) {
|
|
66
|
+
if (seen.has(value)) {
|
|
67
|
+
return "[Circular]";
|
|
68
|
+
}
|
|
69
|
+
seen.add(value);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
return "[Unable to stringify]";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
var log = {
|
|
78
|
+
/**
|
|
79
|
+
* Debug level logging - only outputs when DEBUG=1 or DEBUG=true
|
|
80
|
+
*
|
|
81
|
+
* @param msg - Debug message
|
|
82
|
+
* @param data - Optional data object to include (JSON-stringified)
|
|
83
|
+
*/
|
|
84
|
+
debug: (msg, data) => {
|
|
85
|
+
if (isDebugEnabled(process.env.DEBUG)) {
|
|
86
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
87
|
+
if (data) {
|
|
88
|
+
const sanitizedData = sanitizeObject(data);
|
|
89
|
+
const dataStr = safeStringify(sanitizedData);
|
|
90
|
+
console.error(`[DEBUG] ${sanitizedMsg} ${dataStr}`);
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`[DEBUG] ${sanitizedMsg}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Info level logging
|
|
98
|
+
*
|
|
99
|
+
* @param msg - Info message
|
|
100
|
+
*/
|
|
101
|
+
info: (msg) => {
|
|
102
|
+
console.error(`[INFO] ${sanitizeLogMessage(msg)}`);
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Warning level logging
|
|
106
|
+
*
|
|
107
|
+
* @param msg - Warning message
|
|
108
|
+
*/
|
|
109
|
+
warn: (msg) => {
|
|
110
|
+
console.error(`[WARN] ${sanitizeLogMessage(msg)}`);
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* Error level logging
|
|
114
|
+
*
|
|
115
|
+
* @param msg - Error message
|
|
116
|
+
* @param err - Optional Error object (stack trace shown when DEBUG enabled)
|
|
117
|
+
*/
|
|
118
|
+
error: (msg, err) => {
|
|
119
|
+
console.error(`[ERROR] ${sanitizeLogMessage(msg)}`);
|
|
120
|
+
if (err && isDebugEnabled(process.env.DEBUG)) {
|
|
121
|
+
console.error(sanitizeLogMessage(err.stack || err.message));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
getConfig,
|
|
128
|
+
sanitizeLogMessage,
|
|
129
|
+
log
|
|
130
|
+
};
|