Skip to content

Unchained Engine v4.8.24

Latest

Choose a tag to compare

@pozylon pozylon released this 27 Aug 08:02
· 220 commits to master since this release
d42a0d2

Rolls up every change on the v4.7 and v4.8 lines since the last published release, v4.6.0.

Unchained Engine v4.8

Minor

  • Breaking (MCP deployments): MCP migrated to the MCP TypeScript SDK v2 — the optional peer dependency is renamed from @modelcontextprotocol/sdk to @modelcontextprotocol/server, shrinking the installed MCP footprint from 94 packages to 3 (@unchainedshop/api). The /mcp endpoint is now stateless: every request is served by a fresh per-request MCP server built from that request's authenticated context, so no Mcp-Session-Id is issued or required, GET/DELETE /mcp return 405, the abandoned-session memory growth of the old in-process session map is gone, and the endpoint works multi-replica. Cross-user session reuse is now impossible by construction (there are no sessions), while the per-request 401/403 admin wall — including the WWW-Authenticate/.well-known/oauth-protected-resource metadata — is unchanged. Clients speaking the modern MCP protocol era (2026-07-28, server/discover) are now supported alongside the legacy initialize era; tool schemas in tools/list are otherwise content-identical but declare JSON Schema draft 2020-12 instead of draft-07. Chat no longer needs any @modelcontextprotocol/* client package: the shop-configuration resources are read in-process (admin-gated), and tools continue to flow through @ai-sdk/mcp, which is protocol-native (chat deployments still install @modelcontextprotocol/server, since the tools are served by the engine's own /mcp endpoint). If the new peer is missing, the engine boots with a warning and /mcp answers 503 instead of crashing. The express chat handler was also fixed (it passed an MCP-SDK transport into createMCPClient, which throws since @ai-sdk/mcp v2 — fastify had been fixed earlier) and aligned with fastify (stepCountIs(500), no hardcoded temperature, MCP client closed on all paths). The /mcp endpoint is now covered by an integration test suite (raw JSON-RPC + @ai-sdk/mcp interop); zod ranges in @unchainedshop/api and @unchainedshop/core narrowed to ^4.2.0. See MIGRATION.md.
  • External dependency reduction: the four library replacements remove 9 installed external packages, including sub-dependencies, from a complete production engine install. p-memoize + expiry-map (declared by 6 packages; also orphaning type-fest, mimic-function, map-age-cleaner, p-defer) are replaced by memoizeWithTTL, newly exported from @unchainedshop/utils — same semantics: concurrent callers share the in-flight promise regardless of TTL, the TTL counts from settlement, resolved null is cached, rejections are never cached; @kontsedal/locco is replaced by a MongoDB-backed order lock in @unchainedshop/core-orders using the same locco-locks collection (no migration), same contention semantics (unique key index, upsert-when-expired, 10 × 200 ms retries) — but fail-closed: if the unique index cannot be ensured the module refuses to start instead of handing out locks that do not exclude (previously an index failure was only logged), and a lock timeout now throws a plain Error instead of locco's RetryError; safe-stable-stringify is replaced by safeStringify, newly exported from @unchainedshop/logger — circular references serialize as "[Circular]", shared (non-circular) references serialize normally, BigInt (primitive or boxed) becomes a string; JSON log keys now appear in insertion order instead of sorted, which leaves @unchainedshop/logger with zero runtime dependencies; hashids is replaced by crypto.randomInt over the same unambiguous alphabet — order/quotation/enrollment numbers keep their look but are now always 6 characters (hashids occasionally produced 7); existing persisted numbers are unaffected.
  • Ticketing: @parse/node-apn is now an optional peer dependency. PDF-only ticketing setups no longer install its 21-package subtree; Apple Wallet pass-update pushes throw a descriptive error if the package is missing, and a package that is installed but fails to load logs the real load error. Together with the replacements above, a clean install of all framework packages using npm --omit=dev --omit=optional falls from 93 to 63 external installed package nodes: 30 fewer (−32.3%), covering 28 distinct package names.
  • Dependency metadata cleanup across all packages. Every package now declares exactly what it imports instead of relying on npm workspace hoisting: 13 core-* modules declare @unchainedshop/mongodb, @unchainedshop/core declares zod, api declares graphql and mongodb as peers plus all 17 core-* packages it imports, plugins declares braintree and @noble/curves/@noble/hashes as optional peers; 11 declared-but-never-imported dependencies were removed. Apps that accidentally imported an undeclared transitive may need to declare it themselves now.
  • Quotation.price: Price is now exposed in GraphQL (@unchainedshop/api) — buyers can read the proposed unit price (minor units of the quotation's currency) before accepting, instead of having to add the quotation to the cart to learn the price. Proposal meta and context (the seller's quotationContext, potentially containing internal pricing rationale) deliberately stay off the public type. How a proposed price affects cart calculation remains project-specific (transformItemConfiguration + a project pricing adapter).
  • makeQuotationProposal's quotationContext is now persisted to quotation.context before processing (@unchainedshop/core) — giving modules.quotations.updateContext its intended caller and letting adapters derive proposals from persisted state in quote(). The manual quotation adapter (shop.unchained.quotations.manual) reads quotation.context.price (proposed unit price, minor units) and quotation.context.expires accordingly: makeQuotationProposal(quotationId, { price: 1584 }) persists a priced proposal.
  • Breaking: DocumentDB compatibility mode removed. UNCHAINED_DOCUMENTDB_COMPAT_MODE is no longer read and the isDocumentDBCompatModeEnabled / assertDocumentDBCompatMode exports have been deleted from @unchainedshop/mongodb. Text indexes and $text queries now run unconditionally. Supported text-search targets: MongoDB 4.4+, AWS DocumentDB 5.0+ (text search added Feb 2024), AWS DocumentDB 8.0, FerretDB 2.x. Do not upgrade on AWS DocumentDB ≤4.0 or FerretDB 1.x — startup will fail.
  • MongoDB index refactor across the whole codebase. Compound indexes now cover the common filter+sort shapes on orders, order_positions, order_payments, order_deliveries, order_discounts, quotations, enrollments, products, product_texts, product_variation_texts, product_media_texts, assortments, assortment_texts, assortment_media_texts, users, token_surrogates, payment_credentials, work_queue, and the payment/delivery/warehousing provider collections. Missing single-field indexes were added on order_payments.paymentProviderId, order_deliveries.deliveryProviderId, enrollments.periods.orderId, users.services.web3.verified, users.tags, users.lastLogin.timestamp, token_surrogates.walletAddress, currencies.contractAddress, media_objects.path, and the entire cryptopay_transactions collection (previously unindexed). Redundant singletons covered by new compounds were dropped, and the unused order_discounts.trigger index was removed. See MIGRATION.md for the ops-side cleanup needed on existing production databases.
  • Breaking (cryptopay plugin): CryptopayTransactionsCollection and configureCryptopayModule are now async — the cryptopay plugin now builds indexes on startup. Callers wiring the plugin directly must await cryptopay.configure({ db }).
  • New EVENTS_TTL_SECONDS environment variable to configure the retention of the events collection (previously hardcoded to 2 days).
  • New TTL (15 min) on accounts_webauthn_credentials_creation_requests — ephemeral challenge records no longer accumulate. Insertions now set a created: Date field; existing pre-upgrade records remain until the next manual cleanup.
  • Ticketing package no longer registers its own {meta.cancelled} index on token_surrogates — the index is now owned by @unchainedshop/core-warehousing (as part of a composite with productId).
  • Breaking: modules.warehousing.buildAccessKeyForToken(tokenId) renamed to modules.warehousing.buildAccessKeyFromToken(token). The function now takes the full TokenSurrogate object and returns Promise<string> (no longer nullable). This removes an N+1 lookup pattern in token GraphQL resolvers.
  • Breaking (Payrexx plugin): environment variables DATATRANS_SUCCESS_PATH, DATATRANS_ERROR_PATH, DATATRANS_CANCEL_PATH renamed to PAYREXX_SUCCESS_PATH, PAYREXX_ERROR_PATH, PAYREXX_CANCEL_PATH.
  • Breaking (Stripe plugin): Stripe API pinned to 2026-03-25.dahlia; peer dependency widened to stripe >= 19 < 22. Upgrade stripe to v21 if you depend on this plugin.
  • Breaking (api context): the internal _inLoginMethodResponse marker on user is now the exported symbol IN_LOGIN_RESPONSE from @unchainedshop/api. Custom express/fastify middlewares that read this flag must import the symbol.
  • Breaking (Fastify adapter): session registration now uses rolling: false and saveUninitialized: false, and login/logout flows explicitly call await req.session.save(). Custom Fastify session integrations may need the same treatment.
  • Breaking (Roles): Mutation.setRoles now throws when an unknown role name is supplied (previously accepted silently).
  • New Query.registeredEventTypes: [String!]! for introspecting events registered via @unchainedshop/events.
  • New DataLoader tokenExportStatusLoader exposed on the GraphQL context; token type resolvers use it to batch status lookups.
  • services.warehousing.isTokenInvalidateable({ token, product? }) now accepts an optional product to skip the redundant lookup in hot paths.
  • Mutation.changePassword is now allowed for any logged-in user with the correct current password (was previously admin-only).
  • Schedule parser rewritten to a field-advancing algorithm — same schedule.parse.cron(...) / schedule.parse.text(...) API, dramatically faster for daily and hourly cadences.
  • ESLint upgraded to v10 across the workspace; Stripe SDK updated alongside.
  • Permission resolution refactored for performance (Map-based lookup, early exit).
  • Cart recalculation no longer blocks boot. The startup provider-invalidation sweep, which recalculated every recently-touched cart inline and could spike MongoDB on a cold connection pool after a restart, is replaced by an INVALIDATE_CARTS worker (@unchainedshop/plugins) that recalculates carts sequentially through the work queue. It is autoscheduled monthly (1st at 00:00 local server time — so the year-boundary run lands at local midnight on Jan 1, when new-year tax rates take effect; keep the server's TZ set to the relevant jurisdiction), and setupWorkqueue enqueues an immediate, non-blocking, no-retry work item on boot in the cases that previously recalculated inline (gated by invalidateProviders / UNCHAINED_DISABLE_PROVIDER_INVALIDATION as before).
  • New GC_GUESTS worker (@unchainedshop/plugins) garbage-collects dormant guest users and their carts. A guest is considered stale once both created and lastLogin.timestamp are older than guestUserMaxAgeInDays (env UNCHAINED_GUEST_USER_EXPIRY_DAYS, default 30); collected guests are removed via deleteUserService, which cascade-deletes their open carts. Autoscheduled daily at 02:30. Staleness is decided solely on the user document — neither the user's updated field nor cart updated grants a reprieve, because both are bumped by background/system writes (billing, contact, address normalisation, and the INVALIDATE_CARTS recalculation sweep) that do not represent the guest actually being present and would otherwise keep dead guests alive indefinitely. Caveat: lastLogin.timestamp is the only "presence" signal, and it is bumped only on (re-)login and by the heartbeat mutation. Clients that do not call heartbeat leave it frozen at account creation, so a long-lived guest session with an open cart will be considered stale (and collected together with its cart) once created ages past the cutoff. Call heartbeat periodically from active sessions if you want them to survive GC.

Patch

  • Checkout crash for carts containing quotation items fixed (@unchainedshop/core-quotations): isProposalValid called isExpired without options, which unconditionally destructured { referenceDate } — every checkoutCart with a quotation-based position threw TypeError instead of completing (or instead of the intended "Quotation expired" error). isExpired's options are now optional; checkout of an actually expired quotation now surfaces detailCode: QuotationInvalidError ("Quotation expired or fulfilled…", typo fixed) inside OrderCheckoutError. Regression-covered by a new quotation→cart→checkout integration test.
  • Logged-in users can now view their own quotations (@unchainedshop/api): Query.quotation was admin-only because the loggedIn role never granted viewQuotation — owners could create quotations (and answer them) but not read them back. Now owner-checked via the existing isOwnedQuotation predicate.
  • QUOTATION_REMOVE event is now emitted when requested quotations are deleted during user removal (was registered but never fired).
  • MCP quotation_management tool description no longer advertises the unimplemented REQUEST action.
  • Consistent named errors across all core services (@unchainedshop/core): every service-level throw now carries an error name (surfaced as extensions.detailCode by API wrappers like OrderCheckoutError) instead of a bare Error — new names: ContactMissingError, BillingAddressMissingError, NoDeliveryProviderError, NoPaymentProviderError, QuotationInvalidError, ProductNotFoundError, InvalidQuantityError, ProductWrongStatusError, OrderNotFoundError, PaymentProviderNotFoundError, DeliveryProviderNotFoundError, SignedURLPreparationError, FileNotFoundError (existing NoItemsError unchanged). Messages are unchanged, so message-matching clients keep working.
  • processQuotationService now threads the quotation document like processOrderService threads orders: re-fetches happen only after an adapter hook actually ran, cutting redundant round-trips per lifecycle transition. modules.quotations.updateProposal/updateContext now return the post-update document (previously the pre-update one).
  • Datatrans v2 merchant split rounding fixed (sum now exactly matches order total).
  • PayPal Checkout error wrapping now preserves the original cause.
  • Cryptopay webhook secret check uses timing-safe comparison.
  • Apple Wallet (apple-handler-express) switched from res.writeHead to res.status / res.setHeader for Express 5 compatibility.
  • Apple Wallet PKPass update fix.
  • Fastify Apple Wallet wildcard route corrected (*/*).
  • New Fastify route for the PDF print handler.
  • Bulk importer error messages now include Zod validation issue summaries.
  • Bulk import: sequence is now correctly required.
  • Worker module log output redacts work payload (logs structured fields only).
  • Explicit Fastify session save() after login/impersonation.
  • initDb in @unchainedshop/mongodb now ensures the connection lifecycle.
  • Linting cleanup of packages/platform/bin/unchained.js.

Unchained Engine v4.7

Minor

  • Server-side bulk export system. A new BULK_EXPORT worker (@unchainedshop/plugins) moves product, assortment and filter exports off the client and onto the server, generating CSV files through a configurable exporter factory. Exports can opt individual data in or out, are grouped by type, expose a recent-exports view with a count in the admin-ui side navigation, and produce download links that expire after one hour. Meta export/import is supported for products, filters and assortments.
  • User data export. A default user-export handler exports a user's orders, quotations, reviews and enrollments; the admin-ui gains a user-export flow with configurable fields.
  • Batch user lookup. @unchainedshop/core-users can now find users in batches by usernames and by emails, backing DataLoader-style resolution instead of per-user queries.

Patch

  • Bulk import now batches its payload to avoid large-payload errors, with adjusted product/assortment/filter import normalizers.
  • Fixed Stripe attempting to initialize without a configured apiToken.
  • Fixed a product-export schema issue and a price-export typo.
  • Fixed the bulk-export return value; exported files download automatically on export success.
  • Fixed an orders performance regression (v4.6.2).