/**
* Gateway Dependency Factory — Single source of truth for all shared instances.
*
* Provides a unified GatewayDeps object used by every handler (REST, MCP, A2A).
* On Lambda with DynamoDB env vars, creates DynamoDB-backed stores.
* Otherwise, falls back to in-memory stores for local development.
*/
import { loadConfig } from '../types.js';
import type { GatewayConfig } from '../types.js';
import type { ISessionManager, IMandateStore } from '../interfaces.js';
import { CheckoutSessionManager } from '../ucp/checkout-session.js';
import { MandateStore } from '../ap2/mandate-store.js';
import { MandateVerifier } from '../ap2/verifier.js';
import { Guardrail } from '../middleware/guardrail.js';
import { FeeCollector } from '../middleware/fee-collector.js';
import { ShopifyClient } from '../shopify/client.js';
import { StorefrontAPI } from '../shopify/storefront.js';
import { AdminAPI } from '../shopify/admin.js';
import { DynamoSessionStore } from './session-store.js';
import { DynamoMandateStore } from './mandate-store.js';
import { logger } from '../utils/logger.js';
// ─── Unified Dependency Container ───
export interface GatewayDeps {
config: GatewayConfig;
sessionManager: ISessionManager;
mandateStore: IMandateStore;
feeCollector: FeeCollector;
verifier: MandateVerifier;
guardrail: Guardrail;
storefrontAPI: StorefrontAPI | null;
adminAPI: AdminAPI | null;
}
// ─── Singleton Cache ───
let cached: GatewayDeps | undefined;
/**
* Create or return cached gateway dependencies.
* All handlers (REST, MCP, A2A) MUST use this function
* instead of creating their own singleton instances.
*/
export function getDeps(): GatewayDeps {
if (cached) return cached;
const config = loadConfig();
const isLambda = !!process.env['AWS_LAMBDA_FUNCTION_NAME'];
// ── Session Manager ──
let sessionManager: ISessionManager;
if (isLambda && config.dynamodb.sessionsTable) {
sessionManager = new DynamoSessionStore(config.dynamodb.sessionsTable);
logger.info('Factory: Using DynamoDB session store', { table: config.dynamodb.sessionsTable });
} else {
sessionManager = new CheckoutSessionManager();
logger.info('Factory: Using in-memory session store');
}
// ── Mandate Store ──
let mandateStore: IMandateStore;
if (isLambda && config.dynamodb.mandatesTable) {
mandateStore = new DynamoMandateStore(config.dynamodb.mandatesTable);
logger.info('Factory: Using DynamoDB mandate store', { table: config.dynamodb.mandatesTable });
} else {
mandateStore = new MandateStore();
logger.info('Factory: Using in-memory mandate store');
}
// ── Fee Collector ──
const feeCollector = new FeeCollector({
feeRate: config.gateway.feeRate,
walletAddress: config.gateway.feeWalletAddress,
});
// ── Mandate Verifier ──
const verifier = new MandateVerifier(
config.ap2.verificationPublicKey ?? config.ap2.signingPrivateKey,
);
// ── Guardrail ──
const guardrail = new Guardrail();
// ── Shopify APIs (only when credentials are configured) ──
let storefrontAPI: StorefrontAPI | null = null;
let adminAPI: AdminAPI | null = null;
const { storeDomain, accessToken, storefrontToken } = config.shopify;
if (storeDomain && accessToken && storefrontToken) {
try {
const shopifyClient = new ShopifyClient({
storeDomain,
accessToken,
storefrontToken,
});
storefrontAPI = new StorefrontAPI(shopifyClient);
adminAPI = new AdminAPI(shopifyClient);
logger.info('Factory: Shopify APIs initialized', { storeDomain });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
logger.warn('Factory: Failed to initialize Shopify APIs', { error: message });
}
} else {
logger.info('Factory: Shopify credentials not configured, using mock mode');
}
cached = {
config,
sessionManager,
mandateStore,
feeCollector,
verifier,
guardrail,
storefrontAPI,
adminAPI,
};
logger.info('Factory: GatewayDeps initialized');
return cached;
}
/** Reset cached deps (for testing). */
export function resetDeps(): void {
cached = undefined;
}
/** Inject deps directly (for testing). */
export function setDeps(deps: GatewayDeps): void {
cached = deps;
}