@open-mercato/core 0.10.1-develop.7961.1.d65694a8e6 → 0.10.1-develop.7972.1.03f813eaaf
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/logout.js +3 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +49 -45
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +55 -29
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +8 -3
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +3 -3
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +19 -7
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +5 -5
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js +67 -0
- package/dist/modules/directory/subscribers/seedOrganizationDefaults.js.map +7 -0
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/categories/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/categories/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/context/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/context/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/products/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/products/route.js.map +2 -2
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +1 -1
- package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheInvalidation.js +3 -0
- package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +2 -2
- package/dist/modules/ecommerce/lib/cacheKeys.js +4 -0
- package/dist/modules/ecommerce/lib/cacheKeys.js.map +2 -2
- package/dist/modules/ecommerce/lib/storeContext.js +36 -12
- package/dist/modules/ecommerce/lib/storeContext.js.map +2 -2
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js +14 -0
- package/dist/modules/ecommerce/subscribers/sales-channel-cache-invalidation.js.map +7 -0
- package/dist/modules/integrations/setup.js +34 -24
- package/dist/modules/integrations/setup.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MainMessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js +2 -2
- package/dist/modules/messages/components/message-detail/panels/body-panel.js.map +2 -2
- package/dist/modules/payment_gateways/setup.js +3 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +30 -11
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +3 -3
- package/dist/modules/warranty_claims/setup.js +34 -30
- package/dist/modules/warranty_claims/setup.js.map +2 -2
- package/dist/modules/workflows/setup.js +7 -0
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +3 -3
- package/src/modules/communication_channels/setup.ts +59 -53
- package/src/modules/customer_accounts/services/domainMappingService.ts +57 -29
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +3 -3
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +20 -8
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +7 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/directory/subscribers/seedOrganizationDefaults.ts +84 -0
- package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/categories/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/context/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/products/route.ts +1 -1
- package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +1 -1
- package/src/modules/ecommerce/lib/cacheInvalidation.ts +4 -0
- package/src/modules/ecommerce/lib/cacheKeys.ts +4 -0
- package/src/modules/ecommerce/lib/storeContext.ts +56 -10
- package/src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts +12 -0
- package/src/modules/integrations/setup.ts +41 -24
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/messages/components/message-detail/panels/MainMessageHeader.tsx +3 -3
- package/src/modules/messages/components/message-detail/panels/body-panel.tsx +2 -2
- package/src/modules/payment_gateways/setup.ts +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +55 -28
- package/src/modules/warranty_claims/setup.ts +40 -30
- package/src/modules/workflows/setup.ts +8 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ecommerce/lib/cacheKeys.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { BuyerContext, StoreContext } from './types'\n\n/**\n * The ONLY file in the ecommerce module allowed to resolve the DI `cache` (SPEC-029 \u00A76.1\n * \"Enforcement\", D7). Every storefront key is built by `buildStorefrontCacheKey`; callers pass\n * `parts`, never a raw key. `__tests__/no-raw-cache-access.test.ts` fails on any other access.\n */\n\nexport type StorefrontCacheScope = 'digest' | 'assortment' | 'priceScope' | 'store'\n\nexport type StorefrontCacheKeyOptions = { scope?: StorefrontCacheScope }\n\nexport type StorefrontCacheSetOptions = {\n ttlMs: number\n tags?: string[]\n scope?: StorefrontCacheScope\n}\n\nexport type StorefrontCache = {\n get<T>(parts: string[], options?: StorefrontCacheKeyOptions): Promise<T | null>\n set<T>(parts: string[], value: T, options: StorefrontCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type EcommerceResolutionCacheSetOptions = { ttlMs: number; tags?: string[] }\n\n/**\n * Store-resolution cache (host/slug \u2192 store + default channel binding, \u00A78.1 steps 3\u20135). It runs\n * BEFORE a `StoreContext` exists, so it is tenant-agnostic and MUST NEVER carry buyer data.\n */\nexport type EcommerceResolutionCache = {\n get<T>(parts: string[]): Promise<T | null>\n set<T>(parts: string[], value: T, options: EcommerceResolutionCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\n/**\n * Buyer-context cache (\u00A78, step 6): one entry per (store, customerUserId | anonymous), holding the\n * locale-independent buyer layer only. Tagged per store, per portal user, per identity in\n * `customerIds` and per contributing group so user, membership, terms, group and price events can\n * evict it.\n */\nexport type BuyerContextCache = {\n get(customerUserId: string | null): Promise<BuyerContext | null>\n set(buyer: BuyerContext): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type BuyerContextCacheScope = { tenantId: string; storeId: string }\n\nexport type CacheContainer = { resolve: (name: string) => unknown }\n\nexport const BUYER_CONTEXT_TTL_MS = 60_000\n\nconst STOREFRONT_KEY_NAMESPACE = 'ecommerce:storefront'\nconst RESOLUTION_KEY_NAMESPACE = 'ecommerce:resolution'\nconst BUYER_KEY_NAMESPACE = 'ecommerce:buyer'\nconst ANONYMOUS_SEGMENT = 'anonymous'\nconst NULL_SEGMENT = '-'\n\nconst SCOPE_SEGMENTS: Record<StorefrontCacheScope, string> = {\n digest: 'digest',\n assortment: 'assortment',\n priceScope: 'price-scope',\n store: 'store',\n}\n\nconst logger = createLogger('ecommerce').child({ component: 'storefront-cache' })\n\nfunction encodeSegment(value: string): string {\n return encodeURIComponent(value)\n}\n\nfunction encodeParts(parts: string[]): string {\n if (parts.length === 0) {\n throw new Error('[internal] ecommerce cache keys require at least one part')\n }\n return parts.map(encodeSegment).join(':')\n}\n\nfunction scopeSegments(ctx: StoreContext, scope: StorefrontCacheScope): string[] {\n const storeId = ctx.store.id\n switch (scope) {\n case 'digest':\n return [storeId, ctx.digest]\n case 'assortment':\n return [storeId, ctx.effectiveLocale, ctx.buyer.assortmentScopeHash]\n case 'priceScope':\n return [storeId, ctx.buyer.priceScopeKey, ctx.buyer.customerOverlayId ?? NULL_SEGMENT]\n case 'store':\n return [storeId, ctx.effectiveLocale]\n }\n}\n\nexport function buildStorefrontCacheKey(\n ctx: StoreContext,\n parts: string[],\n options?: StorefrontCacheKeyOptions,\n): string {\n const scope = options?.scope ?? 'digest'\n const prefix = [SCOPE_SEGMENTS[scope], ...scopeSegments(ctx, scope).map(encodeSegment)].join(':')\n return `${STOREFRONT_KEY_NAMESPACE}:${prefix}:${encodeParts(parts)}`\n}\n\nfunction canonicalCacheValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalCacheValue)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .filter((entry) => entry[1] !== undefined)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([key, entry]) => [key, canonicalCacheValue(entry)]),\n )\n }\n return value\n}\n\n/** A fixed-length key part for a structured value: object keys sorted, `undefined` entries dropped, sha256-hashed. */\nexport function storefrontCacheValueHash(value: unknown): string {\n return createHash('sha256').update(JSON.stringify(canonicalCacheValue(value))).digest('hex')\n}\n\nexport function buildEcommerceResolutionCacheKey(parts: string[]): string {\n return `${RESOLUTION_KEY_NAMESPACE}:${encodeParts(parts)}`\n}\n\nexport function buildBuyerContextCacheKey(storeId: string, customerUserId: string | null): string {\n return `${BUYER_KEY_NAMESPACE}:${encodeParts([storeId, customerUserId ?? ANONYMOUS_SEGMENT])}`\n}\n\nexport function ecommerceStoreTag(storeId: string): string {\n return `ecommerce-store:${storeId}`\n}\n\nexport function ecommerceDomainMappingTag(domainMappingId: string): string {\n return `ecommerce-domain-mapping:${domainMappingId}`\n}\n\nexport function ecommerceDomainTag(hostname: string): string {\n return `ecommerce-domain:${hostname}`\n}\n\nexport function ecommerceCustomerTag(customerId: string): string {\n return `customer:${customerId}`\n}\n\n/** One portal user's buyer contexts: evicted when the user is updated (e.g. deactivated) or deleted. */\nexport function ecommerceCustomerUserTag(customerUserId: string): string {\n return `customer-user:${customerUserId}`\n}\n\nexport function ecommerceCustomerGroupTag(customerGroupId: string): string {\n return `customer-group:${customerGroupId}`\n}\n\nexport function catalogProductTag(productId: string): string {\n return `catalog-product:${productId}`\n}\n\n/** Every storefront listing of a tenant: evicted by any product, variant, category or price change. */\nexport function catalogProductsTag(tenantId: string): string {\n return `catalog-products:${tenantId}`\n}\n\nexport function catalogCategoryTag(categoryId: string): string {\n return `catalog-category:${categoryId}`\n}\n\nexport function catalogPriceTag(productId: string): string {\n return `catalog-price:${productId}`\n}\n\n/** Every storefront product entry of a tenant: evicted by an availability policy not tied to one product. */\nexport function storefrontAvailabilityTag(tenantId: string): string {\n return `availability:${tenantId}`\n}\n\n/**\n * Buyer contexts of a tenant that resolved to no customer group (anonymous or unassigned buyers\n * while the tenant has no default group): evicted when a group is created or updated, since that\n * group may have become the default they fall back to.\n */\nexport function ecommerceUngroupedBuyerTag(tenantId: string): string {\n return `customer-group-none:${tenantId}`\n}\n\nexport function buyerContextTags(\n buyer: Pick<BuyerContext, 'customerIds' | 'customerGroupIds'> & { customerUserId?: string | null },\n tenantId?: string,\n): string[] {\n return [\n ...(buyer.customerUserId ? [ecommerceCustomerUserTag(buyer.customerUserId)] : []),\n ...buyer.customerIds.map(ecommerceCustomerTag),\n ...buyer.customerGroupIds.map(ecommerceCustomerGroupTag),\n ...(tenantId && buyer.customerGroupIds.length === 0 ? [ecommerceUngroupedBuyerTag(tenantId)] : []),\n ]\n}\n\nfunction resolveCacheService(container: CacheContainer | null | undefined): CacheStrategy | null {\n if (!container?.resolve) return null\n try {\n const candidate = container.resolve('cache') as CacheStrategy | null | undefined\n if (\n candidate &&\n typeof candidate.get === 'function' &&\n typeof candidate.set === 'function' &&\n typeof candidate.deleteByTags === 'function'\n ) {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\nfunction reportCacheFailure(operation: string, error: unknown): void {\n logger.warn('Ecommerce cache operation failed; degrading to a cache miss', { operation, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.cache_operation_failed',\n attributes: { operation },\n })\n}\n\ntype TenantScopedCacheOptions = {\n cache: CacheStrategy | null\n tenantId: string | null\n baseTags: string[]\n}\n\nfunction createTenantScopedAccess({ cache, tenantId, baseTags }: TenantScopedCacheOptions) {\n return {\n async get<T>(key: string): Promise<T | null> {\n if (!cache) return null\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return (value ?? null) as T | null\n } catch (error) {\n reportCacheFailure('get', error)\n return null\n }\n },\n async set<T>(key: string, value: T, ttlMs: number, tags: string[] | undefined): Promise<void> {\n if (!cache) return\n const mergedTags = Array.from(new Set([...baseTags, ...(tags ?? [])]))\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags: mergedTags }))\n } catch (error) {\n reportCacheFailure('set', error)\n }\n },\n async deleteByTags(tags: string[]): Promise<number> {\n if (!cache || tags.length === 0) return 0\n try {\n return await runWithCacheTenant(tenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n reportCacheFailure('deleteByTags', error)\n return 0\n }\n },\n }\n}\n\nexport function storefrontCache(container: CacheContainer | null | undefined, ctx: StoreContext): StorefrontCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: ctx.tenantId,\n baseTags: [ecommerceStoreTag(ctx.store.id)],\n })\n return {\n get: (parts, options) => access.get(buildStorefrontCacheKey(ctx, parts, options)),\n set: (parts, value, options) =>\n access.set(buildStorefrontCacheKey(ctx, parts, { scope: options.scope }), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport function ecommerceResolutionCache(container: CacheContainer | null | undefined): EcommerceResolutionCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: null,\n baseTags: [],\n })\n return {\n get: (parts) => access.get(buildEcommerceResolutionCacheKey(parts)),\n set: (parts, value, options) =>\n access.set(buildEcommerceResolutionCacheKey(parts), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport type EcommerceCacheInvalidation = { tenantId: string | null; tags: string[] }\n\n/**\n * Event-driven invalidation (\u00A78): evicts `tags` from the tenant-agnostic resolution cache and, when\n * the tenant is known, from that tenant's storefront and buyer-context caches. Tags are tenant\n * scoped by the cache layer, so each scope is cleared inside its own tenant.\n */\nexport async function invalidateEcommerceCacheTags(\n container: CacheContainer | null | undefined,\n invalidation: EcommerceCacheInvalidation,\n): Promise<number> {\n const tags = Array.from(new Set(invalidation.tags.filter((tag) => tag.length > 0)))\n if (tags.length === 0) return 0\n const cache = resolveCacheService(container)\n if (!cache) return 0\n const tenantIds = invalidation.tenantId ? [null, invalidation.tenantId] : [null]\n let deleted = 0\n for (const tenantId of tenantIds) {\n deleted += await createTenantScopedAccess({ cache, tenantId, baseTags: [] }).deleteByTags(tags)\n }\n return deleted\n}\n\nexport function buyerContextCache(\n container: CacheContainer | null | undefined,\n scope: BuyerContextCacheScope,\n): BuyerContextCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: scope.tenantId,\n baseTags: [ecommerceStoreTag(scope.storeId)],\n })\n return {\n get: (customerUserId) => access.get<BuyerContext>(buildBuyerContextCacheKey(scope.storeId, customerUserId)),\n set: (buyer) =>\n access.set(\n buildBuyerContextCacheKey(scope.storeId, buyer.customerUserId),\n buyer,\n BUYER_CONTEXT_TTL_MS,\n buyerContextTags(buyer, scope.tenantId),\n ),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport type { BuyerContext, StoreContext } from './types'\n\n/**\n * The ONLY file in the ecommerce module allowed to resolve the DI `cache` (SPEC-029 \u00A76.1\n * \"Enforcement\", D7). Every storefront key is built by `buildStorefrontCacheKey`; callers pass\n * `parts`, never a raw key. `__tests__/no-raw-cache-access.test.ts` fails on any other access.\n */\n\nexport type StorefrontCacheScope = 'digest' | 'assortment' | 'priceScope' | 'store'\n\nexport type StorefrontCacheKeyOptions = { scope?: StorefrontCacheScope }\n\nexport type StorefrontCacheSetOptions = {\n ttlMs: number\n tags?: string[]\n scope?: StorefrontCacheScope\n}\n\nexport type StorefrontCache = {\n get<T>(parts: string[], options?: StorefrontCacheKeyOptions): Promise<T | null>\n set<T>(parts: string[], value: T, options: StorefrontCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type EcommerceResolutionCacheSetOptions = { ttlMs: number; tags?: string[] }\n\n/**\n * Store-resolution cache (host/slug \u2192 store + default channel binding, \u00A78.1 steps 3\u20135). It runs\n * BEFORE a `StoreContext` exists, so it is tenant-agnostic and MUST NEVER carry buyer data.\n */\nexport type EcommerceResolutionCache = {\n get<T>(parts: string[]): Promise<T | null>\n set<T>(parts: string[], value: T, options: EcommerceResolutionCacheSetOptions): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\n/**\n * Buyer-context cache (\u00A78, step 6): one entry per (store, customerUserId | anonymous), holding the\n * locale-independent buyer layer only. Tagged per store, per portal user, per identity in\n * `customerIds` and per contributing group so user, membership, terms, group and price events can\n * evict it.\n */\nexport type BuyerContextCache = {\n get(customerUserId: string | null): Promise<BuyerContext | null>\n set(buyer: BuyerContext): Promise<void>\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type BuyerContextCacheScope = { tenantId: string; storeId: string }\n\nexport type CacheContainer = { resolve: (name: string) => unknown }\n\nexport const BUYER_CONTEXT_TTL_MS = 60_000\n\nconst STOREFRONT_KEY_NAMESPACE = 'ecommerce:storefront'\nconst RESOLUTION_KEY_NAMESPACE = 'ecommerce:resolution'\nconst BUYER_KEY_NAMESPACE = 'ecommerce:buyer'\nconst ANONYMOUS_SEGMENT = 'anonymous'\nconst NULL_SEGMENT = '-'\n\nconst SCOPE_SEGMENTS: Record<StorefrontCacheScope, string> = {\n digest: 'digest',\n assortment: 'assortment',\n priceScope: 'price-scope',\n store: 'store',\n}\n\nconst logger = createLogger('ecommerce').child({ component: 'storefront-cache' })\n\nfunction encodeSegment(value: string): string {\n return encodeURIComponent(value)\n}\n\nfunction encodeParts(parts: string[]): string {\n if (parts.length === 0) {\n throw new Error('[internal] ecommerce cache keys require at least one part')\n }\n return parts.map(encodeSegment).join(':')\n}\n\nfunction scopeSegments(ctx: StoreContext, scope: StorefrontCacheScope): string[] {\n const storeId = ctx.store.id\n switch (scope) {\n case 'digest':\n return [storeId, ctx.digest]\n case 'assortment':\n return [storeId, ctx.effectiveLocale, ctx.buyer.assortmentScopeHash]\n case 'priceScope':\n return [storeId, ctx.buyer.priceScopeKey, ctx.buyer.customerOverlayId ?? NULL_SEGMENT]\n case 'store':\n return [storeId, ctx.effectiveLocale]\n }\n}\n\nexport function buildStorefrontCacheKey(\n ctx: StoreContext,\n parts: string[],\n options?: StorefrontCacheKeyOptions,\n): string {\n const scope = options?.scope ?? 'digest'\n const prefix = [SCOPE_SEGMENTS[scope], ...scopeSegments(ctx, scope).map(encodeSegment)].join(':')\n return `${STOREFRONT_KEY_NAMESPACE}:${prefix}:${encodeParts(parts)}`\n}\n\nfunction canonicalCacheValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalCacheValue)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .filter((entry) => entry[1] !== undefined)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([key, entry]) => [key, canonicalCacheValue(entry)]),\n )\n }\n return value\n}\n\n/** A fixed-length key part for a structured value: object keys sorted, `undefined` entries dropped, sha256-hashed. */\nexport function storefrontCacheValueHash(value: unknown): string {\n return createHash('sha256').update(JSON.stringify(canonicalCacheValue(value))).digest('hex')\n}\n\nexport function buildEcommerceResolutionCacheKey(parts: string[]): string {\n return `${RESOLUTION_KEY_NAMESPACE}:${encodeParts(parts)}`\n}\n\nexport function buildBuyerContextCacheKey(storeId: string, customerUserId: string | null): string {\n return `${BUYER_KEY_NAMESPACE}:${encodeParts([storeId, customerUserId ?? ANONYMOUS_SEGMENT])}`\n}\n\nexport function ecommerceStoreTag(storeId: string): string {\n return `ecommerce-store:${storeId}`\n}\n\nexport function ecommerceDomainMappingTag(domainMappingId: string): string {\n return `ecommerce-domain-mapping:${domainMappingId}`\n}\n\nexport function ecommerceDomainTag(hostname: string): string {\n return `ecommerce-domain:${hostname}`\n}\n\nexport function ecommerceSalesChannelTag(salesChannelId: string): string {\n return `ecommerce-sales-channel:${salesChannelId}`\n}\n\nexport function ecommerceCustomerTag(customerId: string): string {\n return `customer:${customerId}`\n}\n\n/** One portal user's buyer contexts: evicted when the user is updated (e.g. deactivated) or deleted. */\nexport function ecommerceCustomerUserTag(customerUserId: string): string {\n return `customer-user:${customerUserId}`\n}\n\nexport function ecommerceCustomerGroupTag(customerGroupId: string): string {\n return `customer-group:${customerGroupId}`\n}\n\nexport function catalogProductTag(productId: string): string {\n return `catalog-product:${productId}`\n}\n\n/** Every storefront listing of a tenant: evicted by any product, variant, category or price change. */\nexport function catalogProductsTag(tenantId: string): string {\n return `catalog-products:${tenantId}`\n}\n\nexport function catalogCategoryTag(categoryId: string): string {\n return `catalog-category:${categoryId}`\n}\n\nexport function catalogPriceTag(productId: string): string {\n return `catalog-price:${productId}`\n}\n\n/** Every storefront product entry of a tenant: evicted by an availability policy not tied to one product. */\nexport function storefrontAvailabilityTag(tenantId: string): string {\n return `availability:${tenantId}`\n}\n\n/**\n * Buyer contexts of a tenant that resolved to no customer group (anonymous or unassigned buyers\n * while the tenant has no default group): evicted when a group is created or updated, since that\n * group may have become the default they fall back to.\n */\nexport function ecommerceUngroupedBuyerTag(tenantId: string): string {\n return `customer-group-none:${tenantId}`\n}\n\nexport function buyerContextTags(\n buyer: Pick<BuyerContext, 'customerIds' | 'customerGroupIds'> & { customerUserId?: string | null },\n tenantId?: string,\n): string[] {\n return [\n ...(buyer.customerUserId ? [ecommerceCustomerUserTag(buyer.customerUserId)] : []),\n ...buyer.customerIds.map(ecommerceCustomerTag),\n ...buyer.customerGroupIds.map(ecommerceCustomerGroupTag),\n ...(tenantId && buyer.customerGroupIds.length === 0 ? [ecommerceUngroupedBuyerTag(tenantId)] : []),\n ]\n}\n\nfunction resolveCacheService(container: CacheContainer | null | undefined): CacheStrategy | null {\n if (!container?.resolve) return null\n try {\n const candidate = container.resolve('cache') as CacheStrategy | null | undefined\n if (\n candidate &&\n typeof candidate.get === 'function' &&\n typeof candidate.set === 'function' &&\n typeof candidate.deleteByTags === 'function'\n ) {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\nfunction reportCacheFailure(operation: string, error: unknown): void {\n logger.warn('Ecommerce cache operation failed; degrading to a cache miss', { operation, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.cache_operation_failed',\n attributes: { operation },\n })\n}\n\ntype TenantScopedCacheOptions = {\n cache: CacheStrategy | null\n tenantId: string | null\n baseTags: string[]\n}\n\nfunction createTenantScopedAccess({ cache, tenantId, baseTags }: TenantScopedCacheOptions) {\n return {\n async get<T>(key: string): Promise<T | null> {\n if (!cache) return null\n try {\n const value = await runWithCacheTenant(tenantId, () => cache.get(key))\n return (value ?? null) as T | null\n } catch (error) {\n reportCacheFailure('get', error)\n return null\n }\n },\n async set<T>(key: string, value: T, ttlMs: number, tags: string[] | undefined): Promise<void> {\n if (!cache) return\n const mergedTags = Array.from(new Set([...baseTags, ...(tags ?? [])]))\n try {\n await runWithCacheTenant(tenantId, () => cache.set(key, value, { ttl: ttlMs, tags: mergedTags }))\n } catch (error) {\n reportCacheFailure('set', error)\n }\n },\n async deleteByTags(tags: string[]): Promise<number> {\n if (!cache || tags.length === 0) return 0\n try {\n return await runWithCacheTenant(tenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n reportCacheFailure('deleteByTags', error)\n return 0\n }\n },\n }\n}\n\nexport function storefrontCache(container: CacheContainer | null | undefined, ctx: StoreContext): StorefrontCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: ctx.tenantId,\n baseTags: [ecommerceStoreTag(ctx.store.id)],\n })\n return {\n get: (parts, options) => access.get(buildStorefrontCacheKey(ctx, parts, options)),\n set: (parts, value, options) =>\n access.set(buildStorefrontCacheKey(ctx, parts, { scope: options.scope }), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport function ecommerceResolutionCache(container: CacheContainer | null | undefined): EcommerceResolutionCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: null,\n baseTags: [],\n })\n return {\n get: (parts) => access.get(buildEcommerceResolutionCacheKey(parts)),\n set: (parts, value, options) =>\n access.set(buildEcommerceResolutionCacheKey(parts), value, options.ttlMs, options.tags),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n\nexport type EcommerceCacheInvalidation = { tenantId: string | null; tags: string[] }\n\n/**\n * Event-driven invalidation (\u00A78): evicts `tags` from the tenant-agnostic resolution cache and, when\n * the tenant is known, from that tenant's storefront and buyer-context caches. Tags are tenant\n * scoped by the cache layer, so each scope is cleared inside its own tenant.\n */\nexport async function invalidateEcommerceCacheTags(\n container: CacheContainer | null | undefined,\n invalidation: EcommerceCacheInvalidation,\n): Promise<number> {\n const tags = Array.from(new Set(invalidation.tags.filter((tag) => tag.length > 0)))\n if (tags.length === 0) return 0\n const cache = resolveCacheService(container)\n if (!cache) return 0\n const tenantIds = invalidation.tenantId ? [null, invalidation.tenantId] : [null]\n let deleted = 0\n for (const tenantId of tenantIds) {\n deleted += await createTenantScopedAccess({ cache, tenantId, baseTags: [] }).deleteByTags(tags)\n }\n return deleted\n}\n\nexport function buyerContextCache(\n container: CacheContainer | null | undefined,\n scope: BuyerContextCacheScope,\n): BuyerContextCache {\n const access = createTenantScopedAccess({\n cache: resolveCacheService(container),\n tenantId: scope.tenantId,\n baseTags: [ecommerceStoreTag(scope.storeId)],\n })\n return {\n get: (customerUserId) => access.get<BuyerContext>(buildBuyerContextCacheKey(scope.storeId, customerUserId)),\n set: (buyer) =>\n access.set(\n buildBuyerContextCacheKey(scope.storeId, buyer.customerUserId),\n buyer,\n BUYER_CONTEXT_TTL_MS,\n buyerContextTags(buyer, scope.tenantId),\n ),\n deleteByTags: (tags) => access.deleteByTags(tags),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ecommerceDomainMappingTag,
|
|
15
15
|
ecommerceDomainTag,
|
|
16
16
|
ecommerceResolutionCache,
|
|
17
|
+
ecommerceSalesChannelTag,
|
|
17
18
|
ecommerceStoreTag
|
|
18
19
|
} from "./cacheKeys.js";
|
|
19
20
|
import { resolveStoreLocale } from "./storeLocale.js";
|
|
@@ -50,7 +51,8 @@ const STORE_COLUMNS = [
|
|
|
50
51
|
"c.price_kind_id as channel_price_kind_id",
|
|
51
52
|
"c.assortment_scope as channel_assortment_scope",
|
|
52
53
|
"c.price_sort_fallback as channel_price_sort_fallback",
|
|
53
|
-
"c.require_authentication as channel_require_authentication"
|
|
54
|
+
"c.require_authentication as channel_require_authentication",
|
|
55
|
+
"sc.id as channel_live_sales_channel_id"
|
|
54
56
|
];
|
|
55
57
|
const FORCE_HOST_HEADER = "x-force-host";
|
|
56
58
|
const FORCE_HOST_SECRET_HEADER = "x-force-host-secret";
|
|
@@ -184,7 +186,8 @@ function toCandidate(row) {
|
|
|
184
186
|
defaultCurrencyCode: row.store_default_currency_code,
|
|
185
187
|
settings: parseSettings(row.store_settings, row.store_id)
|
|
186
188
|
};
|
|
187
|
-
const
|
|
189
|
+
const salesChannelMissing = !!row.channel_binding_id && !!row.channel_sales_channel_id && !row.channel_live_sales_channel_id;
|
|
190
|
+
const channel = row.channel_binding_id && row.channel_sales_channel_id && !salesChannelMissing ? {
|
|
188
191
|
channelBindingId: row.channel_binding_id,
|
|
189
192
|
salesChannelId: row.channel_sales_channel_id,
|
|
190
193
|
priceKindId: row.channel_price_kind_id ?? null,
|
|
@@ -193,7 +196,7 @@ function toCandidate(row) {
|
|
|
193
196
|
requireAuthentication: row.channel_require_authentication === true
|
|
194
197
|
} : null;
|
|
195
198
|
const binding = row.binding_id ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true } : null;
|
|
196
|
-
return { store, channel, binding };
|
|
199
|
+
return { store, channel, binding, missingSalesChannelId: salesChannelMissing ? row.channel_sales_channel_id : null };
|
|
197
200
|
}
|
|
198
201
|
async function queryDomainCandidates(em, mapping) {
|
|
199
202
|
const db = em.getKysely();
|
|
@@ -203,6 +206,9 @@ async function queryDomainCandidates(em, mapping) {
|
|
|
203
206
|
).leftJoin(
|
|
204
207
|
"ecommerce_store_channel_bindings as c",
|
|
205
208
|
(join) => join.onRef("c.store_id", "=", "s.id").onRef("c.tenant_id", "=", "s.tenant_id").onRef("c.organization_id", "=", "s.organization_id").on("c.is_default", "=", true).on("c.deleted_at", "is", null)
|
|
209
|
+
).leftJoin(
|
|
210
|
+
"sales_channels as sc",
|
|
211
|
+
(join) => join.onRef("sc.id", "=", "c.sales_channel_id").onRef("sc.tenant_id", "=", "c.tenant_id").onRef("sc.organization_id", "=", "c.organization_id").on("sc.deleted_at", "is", null)
|
|
206
212
|
).select([
|
|
207
213
|
...STORE_COLUMNS,
|
|
208
214
|
"b.id as binding_id",
|
|
@@ -215,10 +221,16 @@ async function querySlugCandidates(em, slug, tenantId) {
|
|
|
215
221
|
let query = db.selectFrom("ecommerce_stores as s").leftJoin(
|
|
216
222
|
"ecommerce_store_channel_bindings as c",
|
|
217
223
|
(join) => join.onRef("c.store_id", "=", "s.id").onRef("c.tenant_id", "=", "s.tenant_id").onRef("c.organization_id", "=", "s.organization_id").on("c.is_default", "=", true).on("c.deleted_at", "is", null)
|
|
224
|
+
).leftJoin(
|
|
225
|
+
"sales_channels as sc",
|
|
226
|
+
(join) => join.onRef("sc.id", "=", "c.sales_channel_id").onRef("sc.tenant_id", "=", "c.tenant_id").onRef("sc.organization_id", "=", "c.organization_id").on("sc.deleted_at", "is", null)
|
|
218
227
|
).select([...STORE_COLUMNS]).where("s.slug", "=", slug).where("s.deleted_at", "is", null);
|
|
219
228
|
if (tenantId) query = query.where("s.tenant_id", "=", tenantId);
|
|
220
229
|
return query.limit(2).execute();
|
|
221
230
|
}
|
|
231
|
+
function salesChannelTags(rows) {
|
|
232
|
+
return rows.flatMap((row) => row.channel_sales_channel_id ? [ecommerceSalesChannelTag(row.channel_sales_channel_id)] : []);
|
|
233
|
+
}
|
|
222
234
|
async function loadDomainCandidates(container, mapping) {
|
|
223
235
|
const cache = ecommerceResolutionCache(container);
|
|
224
236
|
const key = ["domain-mapping", mapping.domainMappingId];
|
|
@@ -229,7 +241,8 @@ async function loadDomainCandidates(container, mapping) {
|
|
|
229
241
|
const tags = [
|
|
230
242
|
ecommerceDomainMappingTag(mapping.domainMappingId),
|
|
231
243
|
ecommerceDomainTag(mapping.hostname),
|
|
232
|
-
...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id))
|
|
244
|
+
...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),
|
|
245
|
+
...salesChannelTags(rows)
|
|
233
246
|
];
|
|
234
247
|
await cache.set(
|
|
235
248
|
key,
|
|
@@ -251,23 +264,34 @@ async function loadSlugCandidates(container, slug, tenantId) {
|
|
|
251
264
|
{ candidates },
|
|
252
265
|
{
|
|
253
266
|
ttlMs: STORE_RESOLUTION_TTL_MS,
|
|
254
|
-
tags:
|
|
267
|
+
tags: Array.from(
|
|
268
|
+
/* @__PURE__ */ new Set([...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)), ...salesChannelTags(rows)])
|
|
269
|
+
)
|
|
255
270
|
}
|
|
256
271
|
);
|
|
257
272
|
}
|
|
258
273
|
return candidates;
|
|
259
274
|
}
|
|
260
|
-
async function reportMisconfiguredStore(container, store) {
|
|
275
|
+
async function reportMisconfiguredStore(container, store, missingSalesChannelId) {
|
|
261
276
|
const cache = ecommerceResolutionCache(container);
|
|
262
277
|
const key = ["misconfigured", "channel-binding-missing", store.id];
|
|
263
278
|
const alreadyReported = await cache.get(key);
|
|
264
279
|
if (alreadyReported) return;
|
|
265
280
|
await cache.set(key, { reportedAt: (/* @__PURE__ */ new Date()).toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
if (missingSalesChannelId) {
|
|
282
|
+
logger.warn("Store default channel binding references a sales channel that no longer exists; answering 503", {
|
|
283
|
+
storeId: store.id,
|
|
284
|
+
tenantId: store.tenantId,
|
|
285
|
+
organizationId: store.organizationId,
|
|
286
|
+
salesChannelId: missingSalesChannelId
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
logger.warn("Store has no default channel binding; answering 503", {
|
|
290
|
+
storeId: store.id,
|
|
291
|
+
tenantId: store.tenantId,
|
|
292
|
+
organizationId: store.organizationId
|
|
293
|
+
});
|
|
294
|
+
}
|
|
271
295
|
try {
|
|
272
296
|
await emitEcommerceEvent(
|
|
273
297
|
"ecommerce.store.misconfigured",
|
|
@@ -297,7 +321,7 @@ async function finalizeCandidate(container, candidate, source, domain, localeCan
|
|
|
297
321
|
}
|
|
298
322
|
if (store.status === "archived") throw new StorefrontResolutionError(410, "store_archived");
|
|
299
323
|
if (!candidate.channel) {
|
|
300
|
-
await reportMisconfiguredStore(container, store);
|
|
324
|
+
await reportMisconfiguredStore(container, store, candidate.missingSalesChannelId ?? null);
|
|
301
325
|
throw new StorefrontResolutionError(503, "store_misconfigured");
|
|
302
326
|
}
|
|
303
327
|
const locale = resolveStoreLocale(store, localeCandidates);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ecommerce/lib/storeContext.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport {\n ECOMMERCE_STORE_SLUG_PATTERN,\n ecommercePriceSortFallbackSchema,\n ecommerceStoreSettingsSchema,\n ecommerceStoreStatusSchema,\n type EcommercePriceSortFallback,\n type EcommerceStoreSettings,\n type EcommerceStoreStatus,\n} from '../data/validators'\nimport { emitEcommerceEvent } from '../events'\nimport {\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceResolutionCache,\n ecommerceStoreTag,\n type CacheContainer,\n} from './cacheKeys'\nimport { resolveStoreLocale, type StoreLocaleCandidates } from './storeLocale'\nimport type { StoreContextChannel, StoreContextStore } from './types'\n\n/**\n * Store layer of storefront resolution (SPEC-029 \u00A74.1 steps 1\u20135 and 7, \u00A76.2, \u00A78, \u00A78.1).\n *\n * Host flow: normalized Host \u2192 `customer_accounts` `domainMappingService.resolveByHostname`\n * (returns `active` mappings only, D1) \u2192 ONE ecommerce query returning every binding of that\n * mapping joined with its store and the store's default channel binding (D6) \u2192 longest\n * `path_prefix` match on a path-segment boundary (R6). The candidate list is cached per mapping.\n *\n * Dev slug flow (D18): `?storeSlug=` only when `OM_ECOMMERCE_DEV_STORE_SLUG` is true, otherwise\n * `400`. A slug is unique within a tenant (\u00A75.1), so slug resolution is tenant-scoped when the\n * caller supplies a tenant and otherwise requires the slug to be unique across tenants \u2014 an\n * ambiguous slug answers `404`.\n *\n * The buyer layer (Step 2.7) composes `ResolvedStore` into a full `StoreContext`.\n */\n\nexport const DEV_STORE_SLUG_ENV = 'OM_ECOMMERCE_DEV_STORE_SLUG'\nexport const STORE_RESOLUTION_TTL_MS = 300_000\nexport const STORE_MISCONFIGURED_THROTTLE_MS = 3_600_000\n\nexport type StorefrontResolutionErrorCode =\n | 'store_slug_not_allowed'\n | 'store_not_found'\n | 'store_draft'\n | 'store_archived'\n | 'store_misconfigured'\n | 'portal_session_scope_mismatch'\n | 'portal_session_invalid'\n\nexport type StorefrontResolutionStatus = 400 | 401 | 403 | 404 | 410 | 503\n\nconst STOREFRONT_RESOLUTION_ERROR_MARKER = Symbol.for('@open-mercato/ecommerce/StorefrontResolutionError')\n\nexport class StorefrontResolutionError extends Error {\n readonly [STOREFRONT_RESOLUTION_ERROR_MARKER] = true\n readonly status: StorefrontResolutionStatus\n readonly code: StorefrontResolutionErrorCode\n\n constructor(status: StorefrontResolutionStatus, code: StorefrontResolutionErrorCode) {\n super(`[internal] storefront store resolution failed: ${code}`)\n this.name = 'StorefrontResolutionError'\n this.status = status\n this.code = code\n }\n}\n\n/**\n * Marker-based so it holds across bundle layers: `storeContextService` is resolved from the DI\n * container, whose registrars may have been loaded from another bundle layer than the route, so\n * the error class it throws is not always the route's `StorefrontResolutionError`.\n */\nexport function isStorefrontResolutionError(error: unknown): error is StorefrontResolutionError {\n return (\n !!error &&\n typeof error === 'object' &&\n (error as Record<symbol, unknown>)[STOREFRONT_RESOLUTION_ERROR_MARKER] === true\n )\n}\n\nexport type ResolvedStoreChannel = StoreContextChannel & {\n assortmentScope: AssortmentScope | null\n requireAuthentication: boolean\n}\n\nexport type ResolvedStoreDomain = {\n domainMappingId: string\n hostname: string\n bindingId: string\n pathPrefix: string | null\n isPrimary: boolean\n}\n\nexport type ResolvedStore = {\n source: 'host' | 'slug'\n store: StoreContextStore\n tenantId: string\n organizationId: string\n channel: ResolvedStoreChannel\n domain: ResolvedStoreDomain | null\n effectiveLocale: string\n requestedLocale: string | null\n currencyCode: string\n}\n\nexport type StoreContextContainer = CacheContainer\n\nexport type ResolveStoreFromRequestOptions = {\n pathname?: string | null\n}\n\nexport type ResolveStoreBySlugOptions = {\n tenantId?: string | null\n locale?: string | null\n headerLocale?: string | null\n acceptLanguage?: string | null\n}\n\ntype DomainMappingResolution = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n status: string\n}\n\ntype DomainMappingResolver = {\n resolveByHostname(hostname: string): Promise<DomainMappingResolution | null>\n}\n\ntype CandidateStore = Omit<StoreContextStore, 'status'> & {\n status: EcommerceStoreStatus\n tenantId: string\n organizationId: string\n}\n\ntype CandidateBinding = { id: string; pathPrefix: string | null; isPrimary: boolean }\n\ntype StoreCandidate = {\n store: CandidateStore\n channel: ResolvedStoreChannel | null\n binding: CandidateBinding | null\n}\n\ntype CachedStoreCandidates = { candidates: StoreCandidate[] }\n\ntype StoreResolutionDatabase = {\n ecommerce_stores: {\n id: string\n tenant_id: string\n organization_id: string\n code: string\n name: string\n slug: string\n status: string\n default_locale: string\n supported_locales: unknown\n default_currency_code: string\n settings: unknown\n deleted_at: Date | null\n }\n ecommerce_store_domain_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n domain_mapping_id: string\n path_prefix: string | null\n is_primary: boolean\n deleted_at: Date | null\n }\n ecommerce_store_channel_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n sales_channel_id: string\n price_kind_id: string | null\n assortment_scope: unknown\n price_sort_fallback: string\n is_default: boolean\n require_authentication: boolean\n deleted_at: Date | null\n }\n}\n\ntype StoreResolutionRow = {\n store_id: string\n store_tenant_id: string\n store_organization_id: string\n store_code: string\n store_name: string\n store_slug: string\n store_status: string\n store_default_locale: string\n store_supported_locales: unknown\n store_default_currency_code: string\n store_settings: unknown\n channel_binding_id: string | null\n channel_sales_channel_id: string | null\n channel_price_kind_id: string | null\n channel_assortment_scope: unknown\n channel_price_sort_fallback: string | null\n channel_require_authentication?: boolean | null\n binding_id?: string | null\n binding_path_prefix?: string | null\n binding_is_primary?: boolean | null\n}\n\nconst STORE_COLUMNS = [\n 's.id as store_id',\n 's.tenant_id as store_tenant_id',\n 's.organization_id as store_organization_id',\n 's.code as store_code',\n 's.name as store_name',\n 's.slug as store_slug',\n 's.status as store_status',\n 's.default_locale as store_default_locale',\n 's.supported_locales as store_supported_locales',\n 's.default_currency_code as store_default_currency_code',\n 's.settings as store_settings',\n 'c.id as channel_binding_id',\n 'c.sales_channel_id as channel_sales_channel_id',\n 'c.price_kind_id as channel_price_kind_id',\n 'c.assortment_scope as channel_assortment_scope',\n 'c.price_sort_fallback as channel_price_sort_fallback',\n 'c.require_authentication as channel_require_authentication',\n] as const\n\nconst FORCE_HOST_HEADER = 'x-force-host'\nconst FORCE_HOST_SECRET_HEADER = 'x-force-host-secret'\nconst SLUG_MAX_LENGTH = 120\n\nconst logger = createLogger('ecommerce').child({ component: 'store-context' })\n\ntype NotFoundReason =\n | 'host_missing'\n | 'domain_routing_unavailable'\n | 'domain_mapping_not_active'\n | 'domain_binding_not_found'\n | 'store_draft_hidden'\n | 'slug_invalid'\n | 'slug_not_found'\n | 'slug_ambiguous'\n\nconst WARN_NOT_FOUND_REASONS: ReadonlySet<NotFoundReason> = new Set([\n 'domain_routing_unavailable',\n 'domain_binding_not_found',\n 'slug_ambiguous',\n])\n\nfunction storeNotFound(reason: NotFoundReason, details: Record<string, unknown>): StorefrontResolutionError {\n const meta = { reason, ...details }\n if (WARN_NOT_FOUND_REASONS.has(reason)) {\n logger.warn('Storefront store not resolved', meta)\n } else {\n logger.debug('Storefront store not resolved', meta)\n }\n return new StorefrontResolutionError(404, 'store_not_found')\n}\n\nfunction tryResolve<T>(container: StoreContextContainer, name: string): T | null {\n try {\n const resolved = container.resolve(name) as T | null | undefined\n return resolved ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveEntityManager(container: StoreContextContainer): EntityManager {\n const em = tryResolve<EntityManager>(container, 'em')\n if (!em) throw new Error('[internal] ecommerce store resolution requires the DI entity manager')\n return em\n}\n\nexport function isDevStoreSlugEnabled(): boolean {\n return parseBooleanWithDefault(process.env[DEV_STORE_SLUG_ENV], false)\n}\n\nexport function normalizeRequestHost(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n const first = raw.split(',')[0]?.trim().toLowerCase() ?? ''\n if (!first.length) return null\n const withoutPort = first.startsWith('[')\n ? first.replace(/^(\\[[^\\]]*\\])(?::\\d+)?$/, '$1')\n : first.replace(/:\\d+$/, '')\n const withoutTrailingDot = withoutPort.replace(/\\.+$/, '')\n return withoutTrailingDot.length ? withoutTrailingDot : null\n}\n\nexport function normalizeRequestPathname(pathname: string | null | undefined): string {\n if (typeof pathname !== 'string' || !pathname.length) return '/'\n const lowered = pathname.toLowerCase()\n const withLeadingSlash = lowered.startsWith('/') ? lowered : `/${lowered}`\n const withoutTrailingSlash = withLeadingSlash.replace(/\\/+$/, '')\n return withoutTrailingSlash.length ? withoutTrailingSlash : '/'\n}\n\nexport function pathPrefixMatches(pathPrefix: string | null, pathname: string): boolean {\n if (!pathPrefix) return true\n const normalizedPrefix = normalizeRequestPathname(pathPrefix)\n if (normalizedPrefix === '/') return true\n return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)\n}\n\nexport function selectLongestPrefixMatch<T extends { binding: { pathPrefix: string | null } | null }>(\n candidates: T[],\n pathname: string,\n): T | null {\n let best: T | null = null\n let bestLength = -1\n for (const candidate of candidates) {\n const pathPrefix = candidate.binding?.pathPrefix ?? null\n if (!pathPrefixMatches(pathPrefix, pathname)) continue\n const length = pathPrefix ? normalizeRequestPathname(pathPrefix).length : 0\n if (length > bestLength) {\n best = candidate\n bestLength = length\n }\n }\n return best\n}\n\nfunction readForcedHost(request: Request): string | null {\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(request.headers.get(FORCE_HOST_SECRET_HEADER), expected)) return null\n const forced = request.headers.get(FORCE_HOST_HEADER)\n return forced && forced.length > 0 ? forced : null\n}\n\nfunction readLocaleCandidates(request: Request, url: URL): StoreLocaleCandidates {\n return {\n queryLocale: url.searchParams.get('locale'),\n headerLocale: request.headers.get('x-locale'),\n acceptLanguage: request.headers.get('accept-language'),\n }\n}\n\nfunction parseJsonColumn(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n return JSON.parse(value)\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseSettings(raw: unknown, storeId: string): EcommerceStoreSettings {\n const parsed = ecommerceStoreSettingsSchema.safeParse(parseJsonColumn(raw) ?? {})\n if (parsed.success) return parsed.data\n logger.warn('Store settings failed validation; serving defaults', { storeId })\n return ecommerceStoreSettingsSchema.parse({})\n}\n\nfunction parseSupportedLocales(raw: unknown, defaultLocale: string): string[] {\n const value = parseJsonColumn(raw)\n const locales = Array.isArray(value)\n ? value.filter((locale): locale is string => typeof locale === 'string' && locale.length > 0)\n : []\n return locales.includes(defaultLocale) ? locales : [defaultLocale, ...locales]\n}\n\nfunction parseStatus(raw: string): EcommerceStoreStatus {\n const parsed = ecommerceStoreStatusSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'draft'\n}\n\nfunction parsePriceSortFallback(raw: string | null): EcommercePriceSortFallback {\n const parsed = ecommercePriceSortFallbackSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'approximate'\n}\n\nfunction parseAssortmentScope(raw: unknown): AssortmentScope | null {\n const value = parseJsonColumn(raw)\n return isRecord(value) ? (value as AssortmentScope) : null\n}\n\nfunction toCandidate(row: StoreResolutionRow): StoreCandidate {\n const store: CandidateStore = {\n id: row.store_id,\n tenantId: row.store_tenant_id,\n organizationId: row.store_organization_id,\n code: row.store_code,\n name: row.store_name,\n slug: row.store_slug,\n status: parseStatus(row.store_status),\n defaultLocale: row.store_default_locale,\n supportedLocales: parseSupportedLocales(row.store_supported_locales, row.store_default_locale),\n defaultCurrencyCode: row.store_default_currency_code,\n settings: parseSettings(row.store_settings, row.store_id),\n }\n const channel: ResolvedStoreChannel | null =\n row.channel_binding_id && row.channel_sales_channel_id\n ? {\n channelBindingId: row.channel_binding_id,\n salesChannelId: row.channel_sales_channel_id,\n priceKindId: row.channel_price_kind_id ?? null,\n priceSortFallback: parsePriceSortFallback(row.channel_price_sort_fallback),\n assortmentScope: parseAssortmentScope(row.channel_assortment_scope),\n requireAuthentication: row.channel_require_authentication === true,\n }\n : null\n const binding: CandidateBinding | null = row.binding_id\n ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true }\n : null\n return { store, channel, binding }\n}\n\nasync function queryDomainCandidates(\n em: EntityManager,\n mapping: DomainMappingResolution,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n return db\n .selectFrom('ecommerce_store_domain_bindings as b')\n .innerJoin('ecommerce_stores as s', (join) =>\n join\n .onRef('s.id', '=', 'b.store_id')\n .onRef('s.tenant_id', '=', 'b.tenant_id')\n .onRef('s.organization_id', '=', 'b.organization_id')\n .on('s.deleted_at', 'is', null),\n )\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .select([\n ...STORE_COLUMNS,\n 'b.id as binding_id',\n 'b.path_prefix as binding_path_prefix',\n 'b.is_primary as binding_is_primary',\n ])\n .where('b.domain_mapping_id', '=', mapping.domainMappingId)\n .where('b.tenant_id', '=', mapping.tenantId)\n .where('b.organization_id', '=', mapping.organizationId)\n .where('b.deleted_at', 'is', null)\n .execute()\n}\n\nasync function querySlugCandidates(\n em: EntityManager,\n slug: string,\n tenantId: string | null,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n let query = db\n .selectFrom('ecommerce_stores as s')\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .select([...STORE_COLUMNS])\n .where('s.slug', '=', slug)\n .where('s.deleted_at', 'is', null)\n if (tenantId) query = query.where('s.tenant_id', '=', tenantId)\n return query.limit(2).execute()\n}\n\nasync function loadDomainCandidates(\n container: StoreContextContainer,\n mapping: DomainMappingResolution,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['domain-mapping', mapping.domainMappingId]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await queryDomainCandidates(resolveEntityManager(container), mapping)\n const candidates = rows.map(toCandidate)\n const tags = [\n ecommerceDomainMappingTag(mapping.domainMappingId),\n ecommerceDomainTag(mapping.hostname),\n ...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n ]\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n { ttlMs: STORE_RESOLUTION_TTL_MS, tags: Array.from(new Set(tags)) },\n )\n return candidates\n}\n\nasync function loadSlugCandidates(\n container: StoreContextContainer,\n slug: string,\n tenantId: string | null,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['slug', tenantId ?? '*', slug]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await querySlugCandidates(resolveEntityManager(container), slug, tenantId)\n const candidates = rows.map(toCandidate)\n if (candidates.length > 0) {\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n {\n ttlMs: STORE_RESOLUTION_TTL_MS,\n tags: candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n },\n )\n }\n return candidates\n}\n\nasync function reportMisconfiguredStore(container: StoreContextContainer, store: CandidateStore): Promise<void> {\n const cache = ecommerceResolutionCache(container)\n const key = ['misconfigured', 'channel-binding-missing', store.id]\n const alreadyReported = await cache.get<{ reportedAt: string }>(key)\n if (alreadyReported) return\n await cache.set(key, { reportedAt: new Date().toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS })\n logger.warn('Store has no default channel binding; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n })\n try {\n await emitEcommerceEvent(\n 'ecommerce.store.misconfigured',\n {\n id: store.id,\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n reason: 'channel_binding_missing',\n },\n { persistent: true, tenantId: store.tenantId, organizationId: store.organizationId },\n )\n } catch (error) {\n logger.warn('Failed to emit store misconfiguration event', { storeId: store.id, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.store_misconfigured_emit_failed',\n attributes: { storeId: store.id },\n })\n }\n}\n\nasync function finalizeCandidate(\n container: StoreContextContainer,\n candidate: StoreCandidate,\n source: ResolvedStore['source'],\n domain: ResolvedStoreDomain | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n const { store } = candidate\n if (store.status === 'draft') {\n if (isDevStoreSlugEnabled()) throw new StorefrontResolutionError(403, 'store_draft')\n throw storeNotFound('store_draft_hidden', { source, storeId: store.id })\n }\n if (store.status === 'archived') throw new StorefrontResolutionError(410, 'store_archived')\n if (!candidate.channel) {\n await reportMisconfiguredStore(container, store)\n throw new StorefrontResolutionError(503, 'store_misconfigured')\n }\n const locale = resolveStoreLocale(store, localeCandidates)\n return {\n source,\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: 'active',\n defaultLocale: store.defaultLocale,\n supportedLocales: store.supportedLocales,\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n channel: candidate.channel,\n domain,\n effectiveLocale: locale.effectiveLocale,\n requestedLocale: locale.requestedLocale,\n currencyCode: store.defaultCurrencyCode,\n }\n}\n\nasync function resolveSlugWithCandidates(\n container: StoreContextContainer,\n rawSlug: string,\n tenantId: string | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n if (!isDevStoreSlugEnabled()) throw new StorefrontResolutionError(400, 'store_slug_not_allowed')\n const slug = rawSlug.trim().toLowerCase()\n if (!slug.length || slug.length > SLUG_MAX_LENGTH || !ECOMMERCE_STORE_SLUG_PATTERN.test(slug)) {\n throw storeNotFound('slug_invalid', {})\n }\n const candidates = await loadSlugCandidates(container, slug, tenantId)\n if (candidates.length === 0) throw storeNotFound('slug_not_found', { slug })\n if (candidates.length > 1) throw storeNotFound('slug_ambiguous', { slug })\n return finalizeCandidate(container, candidates[0], 'slug', null, localeCandidates)\n}\n\nexport async function resolveStoreBySlug(\n container: StoreContextContainer,\n slug: string,\n options: ResolveStoreBySlugOptions = {},\n): Promise<ResolvedStore> {\n return resolveSlugWithCandidates(container, slug, options.tenantId ?? null, {\n queryLocale: options.locale ?? null,\n headerLocale: options.headerLocale ?? null,\n acceptLanguage: options.acceptLanguage ?? null,\n })\n}\n\nexport async function resolveStoreFromRequest(\n container: StoreContextContainer,\n request: Request,\n options: ResolveStoreFromRequestOptions = {},\n): Promise<ResolvedStore> {\n const url = new URL(request.url)\n const localeCandidates = readLocaleCandidates(request, url)\n const storeSlug = url.searchParams.get('storeSlug')\n if (storeSlug !== null) return resolveSlugWithCandidates(container, storeSlug, null, localeCandidates)\n\n const host = normalizeRequestHost(readForcedHost(request) ?? request.headers.get('host'))\n if (!host) throw storeNotFound('host_missing', {})\n const domainMappingService = tryResolve<DomainMappingResolver>(container, 'domainMappingService')\n if (!domainMappingService) throw storeNotFound('domain_routing_unavailable', { host })\n const mapping = await domainMappingService.resolveByHostname(host)\n if (!mapping || mapping.status !== 'active') throw storeNotFound('domain_mapping_not_active', { host })\n\n const candidates = await loadDomainCandidates(container, mapping)\n const pathname = normalizeRequestPathname(options.pathname ?? url.pathname)\n const candidate = selectLongestPrefixMatch(candidates, pathname)\n if (!candidate?.binding) {\n throw storeNotFound('domain_binding_not_found', {\n host,\n domainMappingId: mapping.domainMappingId,\n bindingCount: candidates.length,\n })\n }\n return finalizeCandidate(\n container,\n candidate,\n 'host',\n {\n domainMappingId: mapping.domainMappingId,\n hostname: mapping.hostname,\n bindingId: candidate.binding.id,\n pathPrefix: candidate.binding.pathPrefix,\n isPrimary: candidate.binding.isPrimary,\n },\n localeCandidates,\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { secretEqual } from '@open-mercato/core/modules/customer_accounts/lib/secretCompare'\nimport {\n ECOMMERCE_STORE_SLUG_PATTERN,\n ecommercePriceSortFallbackSchema,\n ecommerceStoreSettingsSchema,\n ecommerceStoreStatusSchema,\n type EcommercePriceSortFallback,\n type EcommerceStoreSettings,\n type EcommerceStoreStatus,\n} from '../data/validators'\nimport { emitEcommerceEvent } from '../events'\nimport {\n ecommerceDomainMappingTag,\n ecommerceDomainTag,\n ecommerceResolutionCache,\n ecommerceSalesChannelTag,\n ecommerceStoreTag,\n type CacheContainer,\n} from './cacheKeys'\nimport { resolveStoreLocale, type StoreLocaleCandidates } from './storeLocale'\nimport type { StoreContextChannel, StoreContextStore } from './types'\n\n/**\n * Store layer of storefront resolution (SPEC-029 \u00A74.1 steps 1\u20135 and 7, \u00A76.2, \u00A78, \u00A78.1).\n *\n * Host flow: normalized Host \u2192 `customer_accounts` `domainMappingService.resolveByHostname`\n * (returns `active` mappings only, D1) \u2192 ONE ecommerce query returning every binding of that\n * mapping joined with its store and the store's default channel binding (D6) \u2192 longest\n * `path_prefix` match on a path-segment boundary (R6). The candidate list is cached per mapping.\n *\n * Dev slug flow (D18): `?storeSlug=` only when `OM_ECOMMERCE_DEV_STORE_SLUG` is true, otherwise\n * `400`. A slug is unique within a tenant (\u00A75.1), so slug resolution is tenant-scoped when the\n * caller supplies a tenant and otherwise requires the slug to be unique across tenants \u2014 an\n * ambiguous slug answers `404`.\n *\n * The buyer layer (Step 2.7) composes `ResolvedStore` into a full `StoreContext`.\n */\n\nexport const DEV_STORE_SLUG_ENV = 'OM_ECOMMERCE_DEV_STORE_SLUG'\nexport const STORE_RESOLUTION_TTL_MS = 300_000\nexport const STORE_MISCONFIGURED_THROTTLE_MS = 3_600_000\n\nexport type StorefrontResolutionErrorCode =\n | 'store_slug_not_allowed'\n | 'store_not_found'\n | 'store_draft'\n | 'store_archived'\n | 'store_misconfigured'\n | 'portal_session_scope_mismatch'\n | 'portal_session_invalid'\n\nexport type StorefrontResolutionStatus = 400 | 401 | 403 | 404 | 410 | 503\n\nconst STOREFRONT_RESOLUTION_ERROR_MARKER = Symbol.for('@open-mercato/ecommerce/StorefrontResolutionError')\n\nexport class StorefrontResolutionError extends Error {\n readonly [STOREFRONT_RESOLUTION_ERROR_MARKER] = true\n readonly status: StorefrontResolutionStatus\n readonly code: StorefrontResolutionErrorCode\n\n constructor(status: StorefrontResolutionStatus, code: StorefrontResolutionErrorCode) {\n super(`[internal] storefront store resolution failed: ${code}`)\n this.name = 'StorefrontResolutionError'\n this.status = status\n this.code = code\n }\n}\n\n/**\n * Marker-based so it holds across bundle layers: `storeContextService` is resolved from the DI\n * container, whose registrars may have been loaded from another bundle layer than the route, so\n * the error class it throws is not always the route's `StorefrontResolutionError`.\n */\nexport function isStorefrontResolutionError(error: unknown): error is StorefrontResolutionError {\n return (\n !!error &&\n typeof error === 'object' &&\n (error as Record<symbol, unknown>)[STOREFRONT_RESOLUTION_ERROR_MARKER] === true\n )\n}\n\nexport type ResolvedStoreChannel = StoreContextChannel & {\n assortmentScope: AssortmentScope | null\n requireAuthentication: boolean\n}\n\nexport type ResolvedStoreDomain = {\n domainMappingId: string\n hostname: string\n bindingId: string\n pathPrefix: string | null\n isPrimary: boolean\n}\n\nexport type ResolvedStore = {\n source: 'host' | 'slug'\n store: StoreContextStore\n tenantId: string\n organizationId: string\n channel: ResolvedStoreChannel\n domain: ResolvedStoreDomain | null\n effectiveLocale: string\n requestedLocale: string | null\n currencyCode: string\n}\n\nexport type StoreContextContainer = CacheContainer\n\nexport type ResolveStoreFromRequestOptions = {\n pathname?: string | null\n}\n\nexport type ResolveStoreBySlugOptions = {\n tenantId?: string | null\n locale?: string | null\n headerLocale?: string | null\n acceptLanguage?: string | null\n}\n\ntype DomainMappingResolution = {\n domainMappingId: string\n hostname: string\n tenantId: string\n organizationId: string\n status: string\n}\n\ntype DomainMappingResolver = {\n resolveByHostname(hostname: string): Promise<DomainMappingResolution | null>\n}\n\ntype CandidateStore = Omit<StoreContextStore, 'status'> & {\n status: EcommerceStoreStatus\n tenantId: string\n organizationId: string\n}\n\ntype CandidateBinding = { id: string; pathPrefix: string | null; isPrimary: boolean }\n\ntype StoreCandidate = {\n store: CandidateStore\n channel: ResolvedStoreChannel | null\n binding: CandidateBinding | null\n missingSalesChannelId?: string | null\n}\n\ntype CachedStoreCandidates = { candidates: StoreCandidate[] }\n\ntype StoreResolutionDatabase = {\n ecommerce_stores: {\n id: string\n tenant_id: string\n organization_id: string\n code: string\n name: string\n slug: string\n status: string\n default_locale: string\n supported_locales: unknown\n default_currency_code: string\n settings: unknown\n deleted_at: Date | null\n }\n ecommerce_store_domain_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n domain_mapping_id: string\n path_prefix: string | null\n is_primary: boolean\n deleted_at: Date | null\n }\n ecommerce_store_channel_bindings: {\n id: string\n tenant_id: string\n organization_id: string\n store_id: string\n sales_channel_id: string\n price_kind_id: string | null\n assortment_scope: unknown\n price_sort_fallback: string\n is_default: boolean\n require_authentication: boolean\n deleted_at: Date | null\n }\n sales_channels: {\n id: string\n tenant_id: string\n organization_id: string\n deleted_at: Date | null\n }\n}\n\ntype StoreResolutionRow = {\n store_id: string\n store_tenant_id: string\n store_organization_id: string\n store_code: string\n store_name: string\n store_slug: string\n store_status: string\n store_default_locale: string\n store_supported_locales: unknown\n store_default_currency_code: string\n store_settings: unknown\n channel_binding_id: string | null\n channel_sales_channel_id: string | null\n channel_price_kind_id: string | null\n channel_assortment_scope: unknown\n channel_price_sort_fallback: string | null\n channel_require_authentication?: boolean | null\n channel_live_sales_channel_id?: string | null\n binding_id?: string | null\n binding_path_prefix?: string | null\n binding_is_primary?: boolean | null\n}\n\nconst STORE_COLUMNS = [\n 's.id as store_id',\n 's.tenant_id as store_tenant_id',\n 's.organization_id as store_organization_id',\n 's.code as store_code',\n 's.name as store_name',\n 's.slug as store_slug',\n 's.status as store_status',\n 's.default_locale as store_default_locale',\n 's.supported_locales as store_supported_locales',\n 's.default_currency_code as store_default_currency_code',\n 's.settings as store_settings',\n 'c.id as channel_binding_id',\n 'c.sales_channel_id as channel_sales_channel_id',\n 'c.price_kind_id as channel_price_kind_id',\n 'c.assortment_scope as channel_assortment_scope',\n 'c.price_sort_fallback as channel_price_sort_fallback',\n 'c.require_authentication as channel_require_authentication',\n 'sc.id as channel_live_sales_channel_id',\n] as const\n\nconst FORCE_HOST_HEADER = 'x-force-host'\nconst FORCE_HOST_SECRET_HEADER = 'x-force-host-secret'\nconst SLUG_MAX_LENGTH = 120\n\nconst logger = createLogger('ecommerce').child({ component: 'store-context' })\n\ntype NotFoundReason =\n | 'host_missing'\n | 'domain_routing_unavailable'\n | 'domain_mapping_not_active'\n | 'domain_binding_not_found'\n | 'store_draft_hidden'\n | 'slug_invalid'\n | 'slug_not_found'\n | 'slug_ambiguous'\n\nconst WARN_NOT_FOUND_REASONS: ReadonlySet<NotFoundReason> = new Set([\n 'domain_routing_unavailable',\n 'domain_binding_not_found',\n 'slug_ambiguous',\n])\n\nfunction storeNotFound(reason: NotFoundReason, details: Record<string, unknown>): StorefrontResolutionError {\n const meta = { reason, ...details }\n if (WARN_NOT_FOUND_REASONS.has(reason)) {\n logger.warn('Storefront store not resolved', meta)\n } else {\n logger.debug('Storefront store not resolved', meta)\n }\n return new StorefrontResolutionError(404, 'store_not_found')\n}\n\nfunction tryResolve<T>(container: StoreContextContainer, name: string): T | null {\n try {\n const resolved = container.resolve(name) as T | null | undefined\n return resolved ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveEntityManager(container: StoreContextContainer): EntityManager {\n const em = tryResolve<EntityManager>(container, 'em')\n if (!em) throw new Error('[internal] ecommerce store resolution requires the DI entity manager')\n return em\n}\n\nexport function isDevStoreSlugEnabled(): boolean {\n return parseBooleanWithDefault(process.env[DEV_STORE_SLUG_ENV], false)\n}\n\nexport function normalizeRequestHost(raw: string | null | undefined): string | null {\n if (typeof raw !== 'string') return null\n const first = raw.split(',')[0]?.trim().toLowerCase() ?? ''\n if (!first.length) return null\n const withoutPort = first.startsWith('[')\n ? first.replace(/^(\\[[^\\]]*\\])(?::\\d+)?$/, '$1')\n : first.replace(/:\\d+$/, '')\n const withoutTrailingDot = withoutPort.replace(/\\.+$/, '')\n return withoutTrailingDot.length ? withoutTrailingDot : null\n}\n\nexport function normalizeRequestPathname(pathname: string | null | undefined): string {\n if (typeof pathname !== 'string' || !pathname.length) return '/'\n const lowered = pathname.toLowerCase()\n const withLeadingSlash = lowered.startsWith('/') ? lowered : `/${lowered}`\n const withoutTrailingSlash = withLeadingSlash.replace(/\\/+$/, '')\n return withoutTrailingSlash.length ? withoutTrailingSlash : '/'\n}\n\nexport function pathPrefixMatches(pathPrefix: string | null, pathname: string): boolean {\n if (!pathPrefix) return true\n const normalizedPrefix = normalizeRequestPathname(pathPrefix)\n if (normalizedPrefix === '/') return true\n return pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)\n}\n\nexport function selectLongestPrefixMatch<T extends { binding: { pathPrefix: string | null } | null }>(\n candidates: T[],\n pathname: string,\n): T | null {\n let best: T | null = null\n let bestLength = -1\n for (const candidate of candidates) {\n const pathPrefix = candidate.binding?.pathPrefix ?? null\n if (!pathPrefixMatches(pathPrefix, pathname)) continue\n const length = pathPrefix ? normalizeRequestPathname(pathPrefix).length : 0\n if (length > bestLength) {\n best = candidate\n bestLength = length\n }\n }\n return best\n}\n\nfunction readForcedHost(request: Request): string | null {\n if (process.env.NODE_ENV !== 'test') return null\n const expected = process.env.FORCE_HOST_SECRET\n if (!expected) return null\n if (!secretEqual(request.headers.get(FORCE_HOST_SECRET_HEADER), expected)) return null\n const forced = request.headers.get(FORCE_HOST_HEADER)\n return forced && forced.length > 0 ? forced : null\n}\n\nfunction readLocaleCandidates(request: Request, url: URL): StoreLocaleCandidates {\n return {\n queryLocale: url.searchParams.get('locale'),\n headerLocale: request.headers.get('x-locale'),\n acceptLanguage: request.headers.get('accept-language'),\n }\n}\n\nfunction parseJsonColumn(value: unknown): unknown {\n if (typeof value !== 'string') return value\n try {\n return JSON.parse(value)\n } catch {\n return null\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction parseSettings(raw: unknown, storeId: string): EcommerceStoreSettings {\n const parsed = ecommerceStoreSettingsSchema.safeParse(parseJsonColumn(raw) ?? {})\n if (parsed.success) return parsed.data\n logger.warn('Store settings failed validation; serving defaults', { storeId })\n return ecommerceStoreSettingsSchema.parse({})\n}\n\nfunction parseSupportedLocales(raw: unknown, defaultLocale: string): string[] {\n const value = parseJsonColumn(raw)\n const locales = Array.isArray(value)\n ? value.filter((locale): locale is string => typeof locale === 'string' && locale.length > 0)\n : []\n return locales.includes(defaultLocale) ? locales : [defaultLocale, ...locales]\n}\n\nfunction parseStatus(raw: string): EcommerceStoreStatus {\n const parsed = ecommerceStoreStatusSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'draft'\n}\n\nfunction parsePriceSortFallback(raw: string | null): EcommercePriceSortFallback {\n const parsed = ecommercePriceSortFallbackSchema.safeParse(raw)\n return parsed.success ? parsed.data : 'approximate'\n}\n\nfunction parseAssortmentScope(raw: unknown): AssortmentScope | null {\n const value = parseJsonColumn(raw)\n return isRecord(value) ? (value as AssortmentScope) : null\n}\n\nfunction toCandidate(row: StoreResolutionRow): StoreCandidate {\n const store: CandidateStore = {\n id: row.store_id,\n tenantId: row.store_tenant_id,\n organizationId: row.store_organization_id,\n code: row.store_code,\n name: row.store_name,\n slug: row.store_slug,\n status: parseStatus(row.store_status),\n defaultLocale: row.store_default_locale,\n supportedLocales: parseSupportedLocales(row.store_supported_locales, row.store_default_locale),\n defaultCurrencyCode: row.store_default_currency_code,\n settings: parseSettings(row.store_settings, row.store_id),\n }\n const salesChannelMissing =\n !!row.channel_binding_id && !!row.channel_sales_channel_id && !row.channel_live_sales_channel_id\n const channel: ResolvedStoreChannel | null =\n row.channel_binding_id && row.channel_sales_channel_id && !salesChannelMissing\n ? {\n channelBindingId: row.channel_binding_id,\n salesChannelId: row.channel_sales_channel_id,\n priceKindId: row.channel_price_kind_id ?? null,\n priceSortFallback: parsePriceSortFallback(row.channel_price_sort_fallback),\n assortmentScope: parseAssortmentScope(row.channel_assortment_scope),\n requireAuthentication: row.channel_require_authentication === true,\n }\n : null\n const binding: CandidateBinding | null = row.binding_id\n ? { id: row.binding_id, pathPrefix: row.binding_path_prefix ?? null, isPrimary: row.binding_is_primary === true }\n : null\n return { store, channel, binding, missingSalesChannelId: salesChannelMissing ? row.channel_sales_channel_id : null }\n}\n\nasync function queryDomainCandidates(\n em: EntityManager,\n mapping: DomainMappingResolution,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n return db\n .selectFrom('ecommerce_store_domain_bindings as b')\n .innerJoin('ecommerce_stores as s', (join) =>\n join\n .onRef('s.id', '=', 'b.store_id')\n .onRef('s.tenant_id', '=', 'b.tenant_id')\n .onRef('s.organization_id', '=', 'b.organization_id')\n .on('s.deleted_at', 'is', null),\n )\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .leftJoin('sales_channels as sc', (join) =>\n join\n .onRef('sc.id', '=', 'c.sales_channel_id')\n .onRef('sc.tenant_id', '=', 'c.tenant_id')\n .onRef('sc.organization_id', '=', 'c.organization_id')\n .on('sc.deleted_at', 'is', null),\n )\n .select([\n ...STORE_COLUMNS,\n 'b.id as binding_id',\n 'b.path_prefix as binding_path_prefix',\n 'b.is_primary as binding_is_primary',\n ])\n .where('b.domain_mapping_id', '=', mapping.domainMappingId)\n .where('b.tenant_id', '=', mapping.tenantId)\n .where('b.organization_id', '=', mapping.organizationId)\n .where('b.deleted_at', 'is', null)\n .execute()\n}\n\nasync function querySlugCandidates(\n em: EntityManager,\n slug: string,\n tenantId: string | null,\n): Promise<StoreResolutionRow[]> {\n const db = em.getKysely<StoreResolutionDatabase>()\n let query = db\n .selectFrom('ecommerce_stores as s')\n .leftJoin('ecommerce_store_channel_bindings as c', (join) =>\n join\n .onRef('c.store_id', '=', 's.id')\n .onRef('c.tenant_id', '=', 's.tenant_id')\n .onRef('c.organization_id', '=', 's.organization_id')\n .on('c.is_default', '=', true)\n .on('c.deleted_at', 'is', null),\n )\n .leftJoin('sales_channels as sc', (join) =>\n join\n .onRef('sc.id', '=', 'c.sales_channel_id')\n .onRef('sc.tenant_id', '=', 'c.tenant_id')\n .onRef('sc.organization_id', '=', 'c.organization_id')\n .on('sc.deleted_at', 'is', null),\n )\n .select([...STORE_COLUMNS])\n .where('s.slug', '=', slug)\n .where('s.deleted_at', 'is', null)\n if (tenantId) query = query.where('s.tenant_id', '=', tenantId)\n return query.limit(2).execute()\n}\n\nfunction salesChannelTags(rows: StoreResolutionRow[]): string[] {\n return rows.flatMap((row) => (row.channel_sales_channel_id ? [ecommerceSalesChannelTag(row.channel_sales_channel_id)] : []))\n}\n\nasync function loadDomainCandidates(\n container: StoreContextContainer,\n mapping: DomainMappingResolution,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['domain-mapping', mapping.domainMappingId]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await queryDomainCandidates(resolveEntityManager(container), mapping)\n const candidates = rows.map(toCandidate)\n const tags = [\n ecommerceDomainMappingTag(mapping.domainMappingId),\n ecommerceDomainTag(mapping.hostname),\n ...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)),\n ...salesChannelTags(rows),\n ]\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n { ttlMs: STORE_RESOLUTION_TTL_MS, tags: Array.from(new Set(tags)) },\n )\n return candidates\n}\n\nasync function loadSlugCandidates(\n container: StoreContextContainer,\n slug: string,\n tenantId: string | null,\n): Promise<StoreCandidate[]> {\n const cache = ecommerceResolutionCache(container)\n const key = ['slug', tenantId ?? '*', slug]\n const cached = await cache.get<CachedStoreCandidates>(key)\n if (cached && Array.isArray(cached.candidates)) return cached.candidates\n const rows = await querySlugCandidates(resolveEntityManager(container), slug, tenantId)\n const candidates = rows.map(toCandidate)\n if (candidates.length > 0) {\n await cache.set<CachedStoreCandidates>(\n key,\n { candidates },\n {\n ttlMs: STORE_RESOLUTION_TTL_MS,\n tags: Array.from(\n new Set([...candidates.map((candidate) => ecommerceStoreTag(candidate.store.id)), ...salesChannelTags(rows)]),\n ),\n },\n )\n }\n return candidates\n}\n\nasync function reportMisconfiguredStore(\n container: StoreContextContainer,\n store: CandidateStore,\n missingSalesChannelId: string | null,\n): Promise<void> {\n const cache = ecommerceResolutionCache(container)\n const key = ['misconfigured', 'channel-binding-missing', store.id]\n const alreadyReported = await cache.get<{ reportedAt: string }>(key)\n if (alreadyReported) return\n await cache.set(key, { reportedAt: new Date().toISOString() }, { ttlMs: STORE_MISCONFIGURED_THROTTLE_MS })\n if (missingSalesChannelId) {\n logger.warn('Store default channel binding references a sales channel that no longer exists; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n salesChannelId: missingSalesChannelId,\n })\n } else {\n logger.warn('Store has no default channel binding; answering 503', {\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n })\n }\n try {\n await emitEcommerceEvent(\n 'ecommerce.store.misconfigured',\n {\n id: store.id,\n storeId: store.id,\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n reason: 'channel_binding_missing',\n },\n { persistent: true, tenantId: store.tenantId, organizationId: store.organizationId },\n )\n } catch (error) {\n logger.warn('Failed to emit store misconfiguration event', { storeId: store.id, err: error })\n getTelemetryRuntime()?.reportError(error, {\n module: 'ecommerce',\n code: 'ecommerce.store_misconfigured_emit_failed',\n attributes: { storeId: store.id },\n })\n }\n}\n\nasync function finalizeCandidate(\n container: StoreContextContainer,\n candidate: StoreCandidate,\n source: ResolvedStore['source'],\n domain: ResolvedStoreDomain | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n const { store } = candidate\n if (store.status === 'draft') {\n if (isDevStoreSlugEnabled()) throw new StorefrontResolutionError(403, 'store_draft')\n throw storeNotFound('store_draft_hidden', { source, storeId: store.id })\n }\n if (store.status === 'archived') throw new StorefrontResolutionError(410, 'store_archived')\n if (!candidate.channel) {\n await reportMisconfiguredStore(container, store, candidate.missingSalesChannelId ?? null)\n throw new StorefrontResolutionError(503, 'store_misconfigured')\n }\n const locale = resolveStoreLocale(store, localeCandidates)\n return {\n source,\n store: {\n id: store.id,\n code: store.code,\n name: store.name,\n slug: store.slug,\n status: 'active',\n defaultLocale: store.defaultLocale,\n supportedLocales: store.supportedLocales,\n defaultCurrencyCode: store.defaultCurrencyCode,\n settings: store.settings,\n },\n tenantId: store.tenantId,\n organizationId: store.organizationId,\n channel: candidate.channel,\n domain,\n effectiveLocale: locale.effectiveLocale,\n requestedLocale: locale.requestedLocale,\n currencyCode: store.defaultCurrencyCode,\n }\n}\n\nasync function resolveSlugWithCandidates(\n container: StoreContextContainer,\n rawSlug: string,\n tenantId: string | null,\n localeCandidates: StoreLocaleCandidates,\n): Promise<ResolvedStore> {\n if (!isDevStoreSlugEnabled()) throw new StorefrontResolutionError(400, 'store_slug_not_allowed')\n const slug = rawSlug.trim().toLowerCase()\n if (!slug.length || slug.length > SLUG_MAX_LENGTH || !ECOMMERCE_STORE_SLUG_PATTERN.test(slug)) {\n throw storeNotFound('slug_invalid', {})\n }\n const candidates = await loadSlugCandidates(container, slug, tenantId)\n if (candidates.length === 0) throw storeNotFound('slug_not_found', { slug })\n if (candidates.length > 1) throw storeNotFound('slug_ambiguous', { slug })\n return finalizeCandidate(container, candidates[0], 'slug', null, localeCandidates)\n}\n\nexport async function resolveStoreBySlug(\n container: StoreContextContainer,\n slug: string,\n options: ResolveStoreBySlugOptions = {},\n): Promise<ResolvedStore> {\n return resolveSlugWithCandidates(container, slug, options.tenantId ?? null, {\n queryLocale: options.locale ?? null,\n headerLocale: options.headerLocale ?? null,\n acceptLanguage: options.acceptLanguage ?? null,\n })\n}\n\nexport async function resolveStoreFromRequest(\n container: StoreContextContainer,\n request: Request,\n options: ResolveStoreFromRequestOptions = {},\n): Promise<ResolvedStore> {\n const url = new URL(request.url)\n const localeCandidates = readLocaleCandidates(request, url)\n const storeSlug = url.searchParams.get('storeSlug')\n if (storeSlug !== null) return resolveSlugWithCandidates(container, storeSlug, null, localeCandidates)\n\n const host = normalizeRequestHost(readForcedHost(request) ?? request.headers.get('host'))\n if (!host) throw storeNotFound('host_missing', {})\n const domainMappingService = tryResolve<DomainMappingResolver>(container, 'domainMappingService')\n if (!domainMappingService) throw storeNotFound('domain_routing_unavailable', { host })\n const mapping = await domainMappingService.resolveByHostname(host)\n if (!mapping || mapping.status !== 'active') throw storeNotFound('domain_mapping_not_active', { host })\n\n const candidates = await loadDomainCandidates(container, mapping)\n const pathname = normalizeRequestPathname(options.pathname ?? url.pathname)\n const candidate = selectLongestPrefixMatch(candidates, pathname)\n if (!candidate?.binding) {\n throw storeNotFound('domain_binding_not_found', {\n host,\n domainMappingId: mapping.domainMappingId,\n bindingCount: candidates.length,\n })\n }\n return finalizeCandidate(\n container,\n candidate,\n 'host',\n {\n domainMappingId: mapping.domainMappingId,\n hostname: mapping.hostname,\n bindingId: candidate.binding.id,\n pathPrefix: candidate.binding.pathPrefix,\n isPrimary: candidate.binding.isPrimary,\n },\n localeCandidates,\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { invalidateForEvent, salesChannelEventTags } from "../lib/cacheInvalidation.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "sales.channel.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "ecommerce:sales-channel-cache-invalidation"
|
|
6
|
+
};
|
|
7
|
+
async function handle(payload, ctx) {
|
|
8
|
+
await invalidateForEvent(payload, ctx, salesChannelEventTags);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
handle as default,
|
|
12
|
+
metadata
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=sales-channel-cache-invalidation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ecommerce/subscribers/sales-channel-cache-invalidation.ts"],
|
|
4
|
+
"sourcesContent": ["import { invalidateForEvent, salesChannelEventTags } from '../lib/cacheInvalidation'\nimport type { EcommerceSubscriberContext } from '../lib/subscriberSupport'\n\nexport const metadata = {\n event: 'sales.channel.*',\n persistent: false,\n id: 'ecommerce:sales-channel-cache-invalidation',\n}\n\nexport default async function handle(payload: unknown, ctx: EcommerceSubscriberContext): Promise<void> {\n await invalidateForEvent(payload, ctx, salesChannelEventTags)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB,6BAA6B;AAGnD,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,OAA8B,SAAkB,KAAgD;AACrG,QAAM,mBAAmB,SAAS,KAAK,qBAAqB;AAC9D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,6 +6,33 @@ function stableUuidFromString(input) {
|
|
|
6
6
|
const hex = Buffer.from(bytes).toString("hex");
|
|
7
7
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
8
8
|
}
|
|
9
|
+
async function ensureHealthProbeSchedule(ctx, legacyScheduleId) {
|
|
10
|
+
const cradle = ctx.container;
|
|
11
|
+
if (typeof cradle.hasRegistration !== "function" || !cradle.hasRegistration("schedulerService")) return;
|
|
12
|
+
const schedulerService = ctx.container.resolve("schedulerService");
|
|
13
|
+
if (legacyScheduleId && typeof schedulerService.unregister === "function") {
|
|
14
|
+
await schedulerService.unregister(legacyScheduleId);
|
|
15
|
+
}
|
|
16
|
+
await schedulerService.register({
|
|
17
|
+
id: stableUuidFromString(`integrations:health-probe:${ctx.tenantId}:${ctx.organizationId}`),
|
|
18
|
+
name: "Integration health probes",
|
|
19
|
+
description: "Runs outbound health checks for enabled integrations every 15 minutes.",
|
|
20
|
+
scopeType: "organization",
|
|
21
|
+
organizationId: ctx.organizationId,
|
|
22
|
+
tenantId: ctx.tenantId,
|
|
23
|
+
scheduleType: "interval",
|
|
24
|
+
scheduleValue: "15m",
|
|
25
|
+
timezone: "UTC",
|
|
26
|
+
targetType: "queue",
|
|
27
|
+
targetQueue: "integration-health-probe",
|
|
28
|
+
targetPayload: {
|
|
29
|
+
scope: { organizationId: ctx.organizationId, tenantId: ctx.tenantId }
|
|
30
|
+
},
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
sourceModule: "integrations",
|
|
33
|
+
isEnabled: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
9
36
|
const setup = {
|
|
10
37
|
defaultRoleFeatures: {
|
|
11
38
|
superadmin: ["integrations.*", "integrations.view", "integrations.manage", "integrations.credentials.manage"],
|
|
@@ -13,30 +40,13 @@ const setup = {
|
|
|
13
40
|
employee: ["integrations.view"]
|
|
14
41
|
},
|
|
15
42
|
async seedDefaults({ container, organizationId, tenantId }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
name: "Integration health probes",
|
|
24
|
-
description: "Runs outbound health checks for enabled integrations every 15 minutes.",
|
|
25
|
-
scopeType: "organization",
|
|
26
|
-
organizationId,
|
|
27
|
-
tenantId,
|
|
28
|
-
scheduleType: "interval",
|
|
29
|
-
scheduleValue: "15m",
|
|
30
|
-
timezone: "UTC",
|
|
31
|
-
targetType: "queue",
|
|
32
|
-
targetQueue: "integration-health-probe",
|
|
33
|
-
targetPayload: {
|
|
34
|
-
scope: { organizationId, tenantId }
|
|
35
|
-
},
|
|
36
|
-
sourceType: "module",
|
|
37
|
-
sourceModule: "integrations",
|
|
38
|
-
isEnabled: true
|
|
39
|
-
});
|
|
43
|
+
await ensureHealthProbeSchedule(
|
|
44
|
+
{ container, organizationId, tenantId },
|
|
45
|
+
stableUuidFromString(`integrations:health-probe:${tenantId}`)
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
async onOrganizationCreated({ container, organizationId, tenantId }) {
|
|
49
|
+
await ensureHealthProbeSchedule({ container, organizationId, tenantId });
|
|
40
50
|
}
|
|
41
51
|
};
|
|
42
52
|
var setup_default = setup;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/integrations/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport crypto from 'node:crypto'\n\nfunction stableUuidFromString(input: string): string {\n const bytes = crypto.createHash('sha256').update(input).digest().subarray(0, 16)\n // RFC4122: set version (5) and variant (10xx)\n bytes[6] = (bytes[6] & 0x0f) | 0x50\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex = Buffer.from(bytes).toString('hex') // 32 hex chars\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\
|
|
5
|
-
"mappings": "AACA,OAAO,YAAY;AAEnB,SAAS,qBAAqB,OAAuB;AACnD,QAAM,QAAQ,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,EAAE;AAE/E,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,OAAO,KAAK,KAAK,EAAE,SAAS,KAAK;AAC7C,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport crypto from 'node:crypto'\n\nfunction stableUuidFromString(input: string): string {\n const bytes = crypto.createHash('sha256').update(input).digest().subarray(0, 16)\n // RFC4122: set version (5) and variant (10xx)\n bytes[6] = (bytes[6] & 0x0f) | 0x50\n bytes[8] = (bytes[8] & 0x3f) | 0x80\n const hex = Buffer.from(bytes).toString('hex') // 32 hex chars\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\ntype SchedulerServiceLike = {\n unregister?: (scheduleId: string) => Promise<void>\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n}\n\nasync function ensureHealthProbeSchedule(ctx: {\n container: import('awilix').AwilixContainer\n organizationId: string\n tenantId: string\n}, legacyScheduleId?: string): Promise<void> {\n const cradle = ctx.container as { hasRegistration?: (name: string) => boolean }\n if (typeof cradle.hasRegistration !== 'function' || !cradle.hasRegistration('schedulerService')) return\n\n const schedulerService = ctx.container.resolve('schedulerService') as SchedulerServiceLike\n if (legacyScheduleId && typeof schedulerService.unregister === 'function') {\n await schedulerService.unregister(legacyScheduleId)\n }\n await schedulerService.register({\n id: stableUuidFromString(`integrations:health-probe:${ctx.tenantId}:${ctx.organizationId}`),\n name: 'Integration health probes',\n description: 'Runs outbound health checks for enabled integrations every 15 minutes.',\n scopeType: 'organization',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'integration-health-probe',\n targetPayload: {\n scope: { organizationId: ctx.organizationId, tenantId: ctx.tenantId },\n },\n sourceType: 'module',\n sourceModule: 'integrations',\n isEnabled: true,\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['integrations.*', 'integrations.view', 'integrations.manage', 'integrations.credentials.manage'],\n admin: ['integrations.*', 'integrations.view', 'integrations.manage', 'integrations.credentials.manage'],\n employee: ['integrations.view'],\n },\n\n async seedDefaults({ container, organizationId, tenantId }) {\n await ensureHealthProbeSchedule(\n { container, organizationId, tenantId },\n stableUuidFromString(`integrations:health-probe:${tenantId}`),\n )\n },\n\n async onOrganizationCreated({ container, organizationId, tenantId }) {\n await ensureHealthProbeSchedule({ container, organizationId, tenantId })\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,YAAY;AAEnB,SAAS,qBAAqB,OAAuB;AACnD,QAAM,QAAQ,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,EAAE;AAE/E,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,OAAO,KAAK,KAAK,EAAE,SAAS,KAAK;AAC7C,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAwBA,eAAe,0BAA0B,KAItC,kBAA0C;AAC3C,QAAM,SAAS,IAAI;AACnB,MAAI,OAAO,OAAO,oBAAoB,cAAc,CAAC,OAAO,gBAAgB,kBAAkB,EAAG;AAEjG,QAAM,mBAAmB,IAAI,UAAU,QAAQ,kBAAkB;AACjE,MAAI,oBAAoB,OAAO,iBAAiB,eAAe,YAAY;AACzE,UAAM,iBAAiB,WAAW,gBAAgB;AAAA,EACpD;AACA,QAAM,iBAAiB,SAAS;AAAA,IAC9B,IAAI,qBAAqB,6BAA6B,IAAI,QAAQ,IAAI,IAAI,cAAc,EAAE;AAAA,IAC1F,MAAM;AAAA,IACN,aAAa;AAAA,IACb,WAAW;AAAA,IACX,gBAAgB,IAAI;AAAA,IACpB,UAAU,IAAI;AAAA,IACd,cAAc;AAAA,IACd,eAAe;AAAA,IACf,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,eAAe;AAAA,MACb,OAAO,EAAE,gBAAgB,IAAI,gBAAgB,UAAU,IAAI,SAAS;AAAA,IACtE;AAAA,IACA,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,WAAW;AAAA,EACb,CAAC;AACH;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,kBAAkB,qBAAqB,uBAAuB,iCAAiC;AAAA,IAC5G,OAAO,CAAC,kBAAkB,qBAAqB,uBAAuB,iCAAiC;AAAA,IACvG,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,WAAW,gBAAgB,SAAS,GAAG;AAC1D,UAAM;AAAA,MACJ,EAAE,WAAW,gBAAgB,SAAS;AAAA,MACtC,qBAAqB,6BAA6B,QAAQ,EAAE;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,EAAE,WAAW,gBAAgB,SAAS,GAAG;AACnE,UAAM,0BAA0B,EAAE,WAAW,gBAAgB,SAAS,CAAC;AAAA,EACzE;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -268,7 +268,7 @@ function MessagesInboxPageClient() {
|
|
|
268
268
|
const total = listQuery.data?.total ?? 0;
|
|
269
269
|
const totalIsCapped = listQuery.data?.totalIsCapped === true;
|
|
270
270
|
const totalPages = listQuery.data?.totalPages ?? 0;
|
|
271
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
271
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-0 max-w-full space-y-4 [&_.isolate]:overflow-x-auto", children: [
|
|
272
272
|
/* @__PURE__ */ jsx(
|
|
273
273
|
DataTable,
|
|
274
274
|
{
|