@endora-commerce/mod-search 0.100.0

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/backend/cli/reindex.d.ts +4 -0
  4. package/dist/backend/cli/reindex.d.ts.map +1 -0
  5. package/dist/backend/cli/reindex.js +32 -0
  6. package/dist/backend/cli/reindex.js.map +1 -0
  7. package/dist/backend/entities/search-phrase-record.entity.d.ts +40 -0
  8. package/dist/backend/entities/search-phrase-record.entity.d.ts.map +1 -0
  9. package/dist/backend/entities/search-phrase-record.entity.js +85 -0
  10. package/dist/backend/entities/search-phrase-record.entity.js.map +1 -0
  11. package/dist/backend/index.d.ts +87 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +153 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/plugin.d.ts +133 -0
  16. package/dist/backend/plugin.d.ts.map +1 -0
  17. package/dist/backend/plugin.js +250 -0
  18. package/dist/backend/plugin.js.map +1 -0
  19. package/dist/backend/routes.admin.d.ts +28 -0
  20. package/dist/backend/routes.admin.d.ts.map +1 -0
  21. package/dist/backend/routes.admin.js +33 -0
  22. package/dist/backend/routes.admin.js.map +1 -0
  23. package/dist/backend/routes.public.d.ts +22 -0
  24. package/dist/backend/routes.public.d.ts.map +1 -0
  25. package/dist/backend/routes.public.js +192 -0
  26. package/dist/backend/routes.public.js.map +1 -0
  27. package/dist/backend/services/embedder-config-resolver.d.ts +25 -0
  28. package/dist/backend/services/embedder-config-resolver.d.ts.map +1 -0
  29. package/dist/backend/services/embedder-config-resolver.js +47 -0
  30. package/dist/backend/services/embedder-config-resolver.js.map +1 -0
  31. package/dist/backend/services/llm-toggle.service.d.ts +50 -0
  32. package/dist/backend/services/llm-toggle.service.d.ts.map +1 -0
  33. package/dist/backend/services/llm-toggle.service.js +78 -0
  34. package/dist/backend/services/llm-toggle.service.js.map +1 -0
  35. package/dist/backend/services/search-event-subscriber.d.ts +125 -0
  36. package/dist/backend/services/search-event-subscriber.d.ts.map +1 -0
  37. package/dist/backend/services/search-event-subscriber.js +113 -0
  38. package/dist/backend/services/search-event-subscriber.js.map +1 -0
  39. package/dist/backend/services/search-indexer.d.ts +346 -0
  40. package/dist/backend/services/search-indexer.d.ts.map +1 -0
  41. package/dist/backend/services/search-indexer.js +670 -0
  42. package/dist/backend/services/search-indexer.js.map +1 -0
  43. package/dist/backend/services/search-phrase-recorder.service.d.ts +25 -0
  44. package/dist/backend/services/search-phrase-recorder.service.d.ts.map +1 -0
  45. package/dist/backend/services/search-phrase-recorder.service.js +89 -0
  46. package/dist/backend/services/search-phrase-recorder.service.js.map +1 -0
  47. package/dist/backend/services/search-query-port.d.ts +29 -0
  48. package/dist/backend/services/search-query-port.d.ts.map +1 -0
  49. package/dist/backend/services/search-query-port.js +20 -0
  50. package/dist/backend/services/search-query-port.js.map +1 -0
  51. package/dist/backend/services/search-query.service.d.ts +205 -0
  52. package/dist/backend/services/search-query.service.d.ts.map +1 -0
  53. package/dist/backend/services/search-query.service.js +346 -0
  54. package/dist/backend/services/search-query.service.js.map +1 -0
  55. package/dist/backend/services/search-reindex-worker.d.ts +27 -0
  56. package/dist/backend/services/search-reindex-worker.d.ts.map +1 -0
  57. package/dist/backend/services/search-reindex-worker.js +13 -0
  58. package/dist/backend/services/search-reindex-worker.js.map +1 -0
  59. package/dist/backend/services/search-suggest.service.d.ts +53 -0
  60. package/dist/backend/services/search-suggest.service.d.ts.map +1 -0
  61. package/dist/backend/services/search-suggest.service.js +64 -0
  62. package/dist/backend/services/search-suggest.service.js.map +1 -0
  63. package/dist/backend/services/suggestion-pricing-enricher.d.ts +62 -0
  64. package/dist/backend/services/suggestion-pricing-enricher.d.ts.map +1 -0
  65. package/dist/backend/services/suggestion-pricing-enricher.js +41 -0
  66. package/dist/backend/services/suggestion-pricing-enricher.js.map +1 -0
  67. package/dist/manifest.d.ts +263 -0
  68. package/dist/manifest.d.ts.map +1 -0
  69. package/dist/manifest.js +300 -0
  70. package/dist/manifest.js.map +1 -0
  71. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts +13 -0
  72. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts.map +1 -0
  73. package/dist/migrations/20260501T123145_search_phrase_records_init.js +40 -0
  74. package/dist/migrations/20260501T123145_search_phrase_records_init.js.map +1 -0
  75. package/dist/migrations/index.d.ts +27 -0
  76. package/dist/migrations/index.d.ts.map +1 -0
  77. package/dist/migrations/index.js +29 -0
  78. package/dist/migrations/index.js.map +1 -0
  79. package/docs/search.md +284 -0
  80. package/i18n/en.json +10 -0
  81. package/i18n/pl.json +10 -0
  82. package/package.json +72 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EACL,oBAAoB,EAIrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AACpF,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAQzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAe5E;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B;;;;OAIG;IACH,oBAAoB,EAAE,wBAAwB,CAAC;IAC/C;;;;;OAKG;IACH,eAAe,EAAE,sBAAsB,CAAC;IACxC;;;;;OAKG;IACH,iBAAiB,EAAE,uBAAuB,CAAC;IAC3C;;;;OAIG;IACH,sBAAsB,EAAE,0BAA0B,CAAC;IACnD;;;;;;;;;;OAUG;IACH,eAAe,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,WAAW,EAAE,qBAAqB,CAAC;IACnC,6EAA6E;IAC7E,oBAAoB,EAAE,iBAAiB,CAAC;IACxC,yDAAyD;IACzD,YAAY,EAAE,mBAAmB,CAAC;IAClC,wBAAwB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,yBAAyB,CAAC;IAC7E;;;;OAIG;IACH,uBAAuB,EAAE,yBAAyB,CAAC;IACnD;;;;OAIG;IACH,aAAa,EAAE,gBAAgB,CAAC;IAChC;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,qBAAqB,CAAC;IAClC,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,cAAc,EAAE,oBAAoB,CAAC;IACrC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,aAAa,EAAE,mBAAmB,CAAC;IACnC,cAAc,EAAE,oBAAoB,CAAC;IACrC;;;;OAIG;IACH,6BAA6B,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAyQ7E"}
@@ -0,0 +1,250 @@
1
+ import { z } from 'zod';
2
+ import { SearchIndexer } from './services/search-indexer.js';
3
+ import { SearchEventSubscriber } from './services/search-event-subscriber.js';
4
+ import { SearchQueryService } from './services/search-query.service.js';
5
+ import { SearchSuggestService, DEFAULT_SUGGESTION_COUNT, DEFAULT_MINIMUM_QUERY_LENGTH, } from './services/search-suggest.service.js';
6
+ import { LlmToggleService } from './services/llm-toggle.service.js';
7
+ import { SearchReindexWorker } from './services/search-reindex-worker.js';
8
+ import { SearchPhraseRecorder } from './services/search-phrase-recorder.service.js';
9
+ import { registerSearchPublicRoutes, } from './routes.public.js';
10
+ import { registerSearchAdminRoutes } from './routes.admin.js';
11
+ import { SettingNotRegistered, SettingOutOfScopeForChannel } from '@endora-commerce/platform/kernel';
12
+ import { effectiveState } from '@endora-commerce/platform/kernel';
13
+ import { enterSystemScope } from '@endora-commerce/platform/kernel';
14
+ import { DEFAULT_INDEX_TASK_TIMEOUT_SECONDS, DEFAULT_REINDEX_INTERVAL_MINUTES, SEARCH_SETTING_CODES, } from '../manifest.js';
15
+ /**
16
+ * Conditions this module has already reported (D-43's warn-once). Module scope
17
+ * and never reset: an out-of-scope setting is a deployment fact, and the reindex
18
+ * timer would otherwise log it on every tick.
19
+ */
20
+ const warnedSearchConditions = new Set();
21
+ function warnOnceForSearch(condition, message) {
22
+ if (warnedSearchConditions.has(condition))
23
+ return;
24
+ warnedSearchConditions.add(condition);
25
+ console.warn(message);
26
+ }
27
+ const numberSchema = z.number();
28
+ export function searchModule(options) {
29
+ /**
30
+ * The indexer's Meilisearch task wait, in milliseconds — this module's own
31
+ * setting, read inside the module (composition checklist item 6) and not a
32
+ * knob a composition root resolves on its behalf. It sits here rather than
33
+ * inside `SearchIndexer` for the same reason
34
+ * `resolveReindexIntervalMinutes` does: this is the one place that already
35
+ * holds the module's settings-read idiom, including which two failures may
36
+ * be absorbed.
37
+ *
38
+ * Re-read per wait rather than captured at boot, so raising it during an
39
+ * incident takes effect on the next task instead of on the next restart.
40
+ *
41
+ * Absorbs only what D-43 allows — a setting the reconciler has not written
42
+ * yet, and a channel-scoped override of a platform-wide read. A shape
43
+ * mismatch or a driver error propagates, and so does `ModuleDisabledError`:
44
+ * neither branch below is a status test.
45
+ */
46
+ const resolveIndexTaskTimeoutMs = async () => {
47
+ try {
48
+ const seconds = await options.settingsService.get(SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS, null, z.number().int().positive());
49
+ return seconds * 1000;
50
+ }
51
+ catch (error) {
52
+ if (error instanceof SettingNotRegistered) {
53
+ return DEFAULT_INDEX_TASK_TIMEOUT_SECONDS * 1000;
54
+ }
55
+ if (error instanceof SettingOutOfScopeForChannel) {
56
+ warnOnceForSearch(`out-of-scope:${SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS}`, `[search] setting "${SEARCH_SETTING_CODES.INDEX_TASK_TIMEOUT_SECONDS}" is scoped ` +
57
+ `to specific sales channels, so it has no platform-wide value — falling back ` +
58
+ `to the manifest default (logged once per process).`);
59
+ return DEFAULT_INDEX_TASK_TIMEOUT_SECONDS * 1000;
60
+ }
61
+ throw error;
62
+ }
63
+ };
64
+ const indexer = new SearchIndexer({
65
+ attributeRead: options.catalogAttributeRead,
66
+ products: options.catalogProducts,
67
+ categories: options.catalogCategories,
68
+ channelMembership: options.salesChannelMembership,
69
+ resolveTaskTimeoutMs: resolveIndexTaskTimeoutMs,
70
+ });
71
+ // Handlers only: `backend.ts` registers them through `ctx.subscribe`, which
72
+ // is what makes this module's effective state decide whether they run.
73
+ const subscriber = new SearchEventSubscriber({
74
+ emFactory: options.emFactory,
75
+ indexer,
76
+ ...(options.settingsService !== undefined
77
+ ? { settingsService: options.settingsService }
78
+ : {}),
79
+ ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),
80
+ });
81
+ const searchQueryService = new SearchQueryService(options.catalogProducts, options.catalogAttributeRead, {}, options.listingPrices, options.organizations);
82
+ // Settings-aware suggest config, with fallback to manifest defaults
83
+ // when the resolver fails for any reason (e.g. Redis hiccup,
84
+ // `SettingNotRegistered`). The popup must never 500 because of a
85
+ // Settings glitch.
86
+ const resolveSuggestConfig = async (ctx) => {
87
+ const fallback = {
88
+ defaultLimit: DEFAULT_SUGGESTION_COUNT,
89
+ minimumQueryLength: DEFAULT_MINIMUM_QUERY_LENGTH,
90
+ };
91
+ try {
92
+ // Feature 053 / FR-002: per-channel settings key off the channel
93
+ // resolved once upstream — no re-resolution here.
94
+ const channelId = ctx.resolvedChannel.id;
95
+ const [defaultLimit, minimumQueryLength] = await Promise.all([
96
+ options.settingsService.get(SEARCH_SETTING_CODES.POPUP_SUGGESTION_COUNT, channelId, numberSchema),
97
+ options.settingsService.get(SEARCH_SETTING_CODES.POPUP_MINIMUM_QUERY_LENGTH, channelId, numberSchema),
98
+ ]);
99
+ return { defaultLimit, minimumQueryLength };
100
+ }
101
+ catch {
102
+ return fallback;
103
+ }
104
+ };
105
+ const suggestService = new SearchSuggestService(searchQueryService, resolveSuggestConfig);
106
+ const llmToggleService = new LlmToggleService(options.emFactory, options.settingsService, options.settingsAdminService, options.credentials);
107
+ /**
108
+ * The sweep's cadence is this module's own setting, so it reads it itself
109
+ * rather than taking a resolver from a composition root — which is where the
110
+ * identical `try`/`catch`-to-the-manifest-default lived before T123.
111
+ *
112
+ * Read **platform-wide** (feature 072, D-41). A background reindex is one
113
+ * timer in one process covering every channel's index, so there is no channel
114
+ * whose value it could sensibly take; it used to pass the literal `'default'`,
115
+ * a channel **code** against a `uuid` column, so PostgreSQL rejected every
116
+ * read, the `catch` answered with the constant, and an operator changing the
117
+ * cadence changed nothing.
118
+ *
119
+ * Absorbs only what D-43 allows; a shape mismatch or a driver error
120
+ * propagates to the caller, which logs it and keeps polling.
121
+ */
122
+ const resolveReindexIntervalMinutes = async () => {
123
+ try {
124
+ return await options.settingsService.get(SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES, null, z.number().int().nonnegative());
125
+ }
126
+ catch (error) {
127
+ if (error instanceof SettingNotRegistered)
128
+ return DEFAULT_REINDEX_INTERVAL_MINUTES;
129
+ if (error instanceof SettingOutOfScopeForChannel) {
130
+ warnOnceForSearch(`out-of-scope:${SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES}`, `[search] setting "${SEARCH_SETTING_CODES.REINDEX_INTERVAL_MINUTES}" is scoped ` +
131
+ `to specific sales channels, so it has no platform-wide value — falling back ` +
132
+ `to the manifest default (logged once per process).`);
133
+ return DEFAULT_REINDEX_INTERVAL_MINUTES;
134
+ }
135
+ throw error;
136
+ }
137
+ };
138
+ const phraseRecorder = new SearchPhraseRecorder(options.emFactory, options.settingsService);
139
+ const reindexWorker = new SearchReindexWorker({
140
+ emFactory: options.emFactory,
141
+ indexer,
142
+ });
143
+ return {
144
+ handle: {
145
+ indexer,
146
+ subscriber,
147
+ searchQueryService,
148
+ suggestService,
149
+ reindexWorker,
150
+ phraseRecorder,
151
+ llmToggleService,
152
+ resolveReindexIntervalMinutes,
153
+ },
154
+ plugin: async (app) => {
155
+ // US1 — typeahead popup feed.
156
+ // US3 — analytics ingest. Both live in routes.public.ts.
157
+ await registerSearchPublicRoutes(app, {
158
+ suggestService,
159
+ phraseRecorder,
160
+ enrichSuggestionPricing: options.enrichSuggestionPricing,
161
+ });
162
+ // US2 — LLM toggle wrapper. Mounts only when the admin gate +
163
+ // settings admin service are both wired (test-server passes them).
164
+ await registerSearchAdminRoutes(app, {
165
+ llmToggleService,
166
+ reindexWorker,
167
+ requireAdmin: options.requireAdmin,
168
+ resolveAdminAuditContext: options.resolveAdminAuditContext,
169
+ });
170
+ // Periodic full Meilisearch reindex (FR — keep the catalogue in sync
171
+ // even when an incremental event was missed). The interval is read
172
+ // from Settings on every tick via a self-rescheduling timer, so an
173
+ // operator changing `search.reindex_interval_minutes` takes effect on
174
+ // the next cycle without a restart. A value <= 0 disables the sweep but
175
+ // the timer keeps polling the setting so it can be re-enabled live.
176
+ if (options.enableReindexScheduler) {
177
+ const DISABLED_POLL_MS = 60_000;
178
+ let timer;
179
+ let stopped = false;
180
+ const scheduleNext = (delayMs) => {
181
+ if (stopped)
182
+ return;
183
+ timer = setTimeout(tick, delayMs);
184
+ if (typeof timer.unref === 'function')
185
+ timer.unref();
186
+ };
187
+ const tick = () => {
188
+ // Presence is decided here, before any work and outside the `try`s
189
+ // below (issue #126). A timer callback has nowhere to throw to, so
190
+ // `ModuleDisabledError` cannot propagate from it and must be decided;
191
+ // asked from inside one of those `try`s, a switched-off module and a
192
+ // failed sweep would land in the same handler.
193
+ //
194
+ // The reschedule is deliberate and is not a leak: this timer *is* the
195
+ // loop, so returning without re-arming would stop the scheduler for
196
+ // the life of the process and no re-enable would bring it back. It is
197
+ // the same shape as the `minutes <= 0` branch below — keep polling,
198
+ // do nothing.
199
+ if (!effectiveState.isPresent('search')) {
200
+ scheduleNext(DISABLED_POLL_MS);
201
+ return;
202
+ }
203
+ // Feature 072 (FR-020), issue #128 — the timer is the entry point, so
204
+ // the scope opens here: the setting read and the sweep both query, and
205
+ // outside a scope they would run on whatever tenancy the ambient store
206
+ // happened to hold. It goes around the *work* and not around the guard
207
+ // above, and the reschedule stays outside it, because this timer is the
208
+ // loop — a tick that failed to re-arm would stop the scheduler for the
209
+ // life of the process. `reindex()` is also reachable from the admin
210
+ // route, where the request's scope is already open and a second one
211
+ // would be wrong; that is why the scope lives at the timer rather than
212
+ // inside the worker.
213
+ void enterSystemScope('search: periodic reindex sweep', async () => {
214
+ let minutes = 0;
215
+ try {
216
+ minutes = await resolveReindexIntervalMinutes();
217
+ }
218
+ catch (err) {
219
+ app.log.error({ err }, 'search reindex: failed to resolve interval setting');
220
+ }
221
+ // Disabled — poll on, so a re-enable takes effect live.
222
+ if (!Number.isFinite(minutes) || minutes <= 0)
223
+ return DISABLED_POLL_MS;
224
+ try {
225
+ const result = await reindexWorker.reindex();
226
+ app.log.info({ result }, 'search reindex sweep completed');
227
+ }
228
+ catch (err) {
229
+ app.log.error({ err }, 'search reindex sweep failed');
230
+ }
231
+ return minutes * 60_000;
232
+ }, { entryPoint: 'interval' })
233
+ .then(scheduleNext)
234
+ .catch((err) => {
235
+ app.log.error({ err }, 'search reindex tick failed');
236
+ scheduleNext(DISABLED_POLL_MS);
237
+ });
238
+ };
239
+ // Kick off the first poll without an immediate reindex at boot.
240
+ scheduleNext(DISABLED_POLL_MS);
241
+ app.addHook('onClose', async () => {
242
+ stopped = true;
243
+ if (timer)
244
+ clearTimeout(timer);
245
+ });
246
+ }
247
+ },
248
+ };
249
+ }
250
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import { type SettingsAdminAuditContext } from '@endora-commerce/contracts';
3
+ import type { LlmToggleService } from './services/llm-toggle.service.js';
4
+ import type { SearchReindexWorker } from './services/search-reindex-worker.js';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ /**
7
+ * Admin HTTP surface — feature 006 / US2 (T025).
8
+ *
9
+ * Mounts under `/api/v1/admin/search/*`:
10
+ * - POST /llm/toggle — flip `search.llm.enabled` on/off with cross-
11
+ * setting validation. Refuses to enable for any channel whose
12
+ * embedder.* triplet is incomplete (FR-011).
13
+ *
14
+ * The three embedder.* values themselves are still saved through the
15
+ * generic Settings admin route (`PUT /api/v1/admin/settings/:code/value`).
16
+ * That keeps the per-field audit trail intact while the toggle remains
17
+ * the single guarded entry point for `enabled`.
18
+ */
19
+ export interface SearchAdminDeps {
20
+ llmToggleService: LlmToggleService;
21
+ /** Backs the manual "Reindex now" admin action. */
22
+ reindexWorker: SearchReindexWorker;
23
+ requireAdmin: RequireAdminFactory;
24
+ /** Resolves the audit actor from the request; mirrors Settings' shape. */
25
+ resolveAdminAuditContext?: (req: FastifyRequest) => SettingsAdminAuditContext;
26
+ }
27
+ export declare function registerSearchAdminRoutes(app: FastifyInstance, deps: SearchAdminDeps): Promise<void>;
28
+ //# sourceMappingURL=routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAA0B,KAAK,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,mDAAmD;IACnD,aAAa,EAAE,mBAAmB,CAAC;IACnC,YAAY,EAAE,mBAAmB,CAAC;IAClC,0EAA0E;IAC1E,wBAAwB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,yBAAyB,CAAC;CAC/E;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAwCf"}
@@ -0,0 +1,33 @@
1
+ import { LlmToggleRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerSearchAdminRoutes(app, deps) {
3
+ const { llmToggleService, reindexWorker, requireAdmin, resolveAdminAuditContext } = deps;
4
+ // Manual full Meilisearch reindex — the on-demand equivalent of the
5
+ // periodic `search.reindex_interval_minutes` sweep. Runs synchronously and
6
+ // returns a per-run summary. Gated by the existing `search:write`
7
+ // permission (no new permission code introduced).
8
+ app.post('/api/v1/admin/search/reindex', { preHandler: requireAdmin('search:write') }, async (_request, reply) => {
9
+ const result = await reindexWorker.reindex();
10
+ return reply.send(result);
11
+ });
12
+ app.post('/api/v1/admin/search/llm/toggle', { preHandler: requireAdmin('search:write') }, async (request) => {
13
+ const body = LlmToggleRequestSchema.parse(request.body);
14
+ const actor = resolveAdminAuditContext
15
+ ? resolveAdminAuditContext(request)
16
+ : { actorAdminUserId: null };
17
+ const result = await llmToggleService.toggle({
18
+ enabled: body.enabled,
19
+ ...(body.salesChannelCodes !== undefined
20
+ ? { salesChannelCodes: body.salesChannelCodes }
21
+ : {}),
22
+ expectedVersion: body.expectedVersion ?? null,
23
+ actor,
24
+ });
25
+ return {
26
+ code: 'search.llm.enabled',
27
+ enabled: result.enabled,
28
+ newVersion: result.newVersion,
29
+ appliedChannelIds: result.appliedChannelIds,
30
+ };
31
+ });
32
+ }
33
+ //# sourceMappingURL=routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAkC,MAAM,4BAA4B,CAAC;AA4BpG,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAoB,EACpB,IAAqB;IAErB,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,wBAAwB,EAAE,GAAG,IAAI,CAAC;IAEzF,oEAAoE;IACpE,2EAA2E;IAC3E,kEAAkE;IAClE,kDAAkD;IAClD,GAAG,CAAC,IAAI,CACN,8BAA8B,EAC9B,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,EAC5C,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,iCAAiC,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,EAC5C,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,sBAAsB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,KAAK,GAA8B,wBAAwB;YAC/D,CAAC,CAAC,wBAAwB,CAAC,OAAO,CAAC;YACnC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC;YAC3C,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,SAAS;gBACtC,CAAC,CAAC,EAAE,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE;gBAC/C,CAAC,CAAC,EAAE,CAAC;YACP,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;YAC7C,KAAK;SACN,CAAC,CAAC;QACH,OAAO;YACL,IAAI,EAAE,oBAA6B;YACnC,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;SAC5C,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type ProductSummary, type SearchSuggestItem } from '@endora-commerce/contracts';
3
+ import { type SearchSuggestService } from './services/search-suggest.service.js';
4
+ import type { SearchPhraseRecorder } from './services/search-phrase-recorder.service.js';
5
+ export type SuggestionPricingEnricher = (items: ProductSummary[], ctx: {
6
+ /** The request's resolved sales channel (feature 053 / FR-002). */
7
+ resolvedChannel: {
8
+ id: string;
9
+ defaultCurrency: string;
10
+ };
11
+ /** Organization of the signed-in customer, or null for guests. */
12
+ organizationId?: string | null;
13
+ }) => Promise<SearchSuggestItem[]>;
14
+ export interface SearchPublicDeps {
15
+ suggestService: SearchSuggestService;
16
+ /** When provided, `POST /api/v1/search/record` is mounted (US3). */
17
+ phraseRecorder?: SearchPhraseRecorder;
18
+ /** When provided, suggestions carry the per-customer resolved price. */
19
+ enrichSuggestionPricing?: SuggestionPricingEnricher;
20
+ }
21
+ export declare function registerSearchPublicRoutes(app: FastifyInstance, deps: SearchPublicDeps): Promise<void>;
22
+ //# sourceMappingURL=routes.public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.d.ts","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAE/D,OAAO,EAOL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,4BAA4B,CAAC;AAQpC,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AA+CzF,MAAM,MAAM,yBAAyB,GAAG,CACtC,KAAK,EAAE,cAAc,EAAE,EACvB,GAAG,EAAE;IACH,mEAAmE;IACnE,eAAe,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,KACE,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,oBAAoB,CAAC;IACrC,oEAAoE;IACpE,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,wEAAwE;IACxE,uBAAuB,CAAC,EAAE,yBAAyB,CAAC;CACrD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAmHf"}
@@ -0,0 +1,192 @@
1
+ import { z } from 'zod';
2
+ import { ERROR_CODES, RecordPhraseRequestSchema, SearchSuggestQuerySchema, SEARCH_PHRASE_MAX_LENGTH, SEARCH_SUGGEST_LIMIT_MAX, } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import { markPersonalisedPricing, productAudienceOf } from '@endora-commerce/platform/http';
5
+ import { currentSalesChannel, getResolvedChannel, } from '@endora-commerce/platform/kernel';
6
+ import { QueryTooShort, SearchBackendUnavailable, } from './services/search-suggest.service.js';
7
+ /**
8
+ * Public HTTP surface — feature 006 / US1 (T013) and US3 (T036).
9
+ *
10
+ * Routes mounted under `/api/v1/search/*`:
11
+ * - GET /suggest — typeahead popup feed.
12
+ * - POST /record — analytics ingest. Returns 202 immediately;
13
+ * persistence is queued via the recorder service
14
+ * so the storefront response is never delayed
15
+ * (FR-015).
16
+ *
17
+ * Sales-channel scoping mirrors the catalog public routes: the channel
18
+ * is resolved from the request scope (filled by the resolver
19
+ * middleware) with a header fallback for the contract-level read in
20
+ * `readContext`.
21
+ */
22
+ const acceptLanguageHeaderSchema = z.string().optional();
23
+ /**
24
+ * Enriches raw suggestions with the per-customer price-list resolution so the
25
+ * typeahead popup can show the price the searching user would actually pay,
26
+ * honouring their price list and price-visibility. Wired from the composition
27
+ * root (where the price-lists `PricingService` lives). When absent, the popup
28
+ * degrades to the legacy `ProductSummary.price` projection.
29
+ */
30
+ /**
31
+ * The signed-in customer's organization, or `null`.
32
+ *
33
+ * `request.actor` is not on `FastifyRequest`: it is a declaration-merging
34
+ * augmentation the `auth` module contributes (`modules/auth/plugin.ts`). Inside
35
+ * `backend/src` it arrived ambiently through the host's own `types` graph, so
36
+ * nothing in this module ever named it; a package compiles against its own
37
+ * manifest and `auth` publishes nothing a package can name, so the property is
38
+ * simply not there (TS2339). Read structurally and narrowed rather than
39
+ * asserted — an absent actor answers `null`, which is the guest price, exactly
40
+ * as before.
41
+ */
42
+ function customerOrganizationId(request) {
43
+ const { actor } = request;
44
+ if (actor?.kind !== 'customer')
45
+ return null;
46
+ return actor.organizationId ?? null;
47
+ }
48
+ export async function registerSearchPublicRoutes(app, deps) {
49
+ const { suggestService, phraseRecorder, enrichSuggestionPricing } = deps;
50
+ // POST /api/v1/search/record — feature 006 / US3 / T036.
51
+ // Mounted only when a recorder was wired (search/plugin.ts gates
52
+ // this on `settingsService` being provided so the recorder can read
53
+ // the per-channel minimum-length threshold).
54
+ if (phraseRecorder) {
55
+ app.post('/api/v1/search/record', async (request, reply) => {
56
+ const parseResult = RecordPhraseRequestSchema.safeParse(request.body);
57
+ if (!parseResult.success) {
58
+ // The two negative paths the contract calls out
59
+ // (PHRASE_REQUIRED, PHRASE_TOO_LONG) are derived from the Zod
60
+ // issue codes so the wire surface is stable.
61
+ const issue = parseResult.error.issues[0];
62
+ const code = issue?.path[0] === 'phrase' && issue?.code === 'too_big'
63
+ ? ERROR_CODES.PHRASE_TOO_LONG
64
+ : issue?.path[0] === 'resultCount'
65
+ ? ERROR_CODES.RESULT_COUNT_INVALID
66
+ : ERROR_CODES.PHRASE_REQUIRED;
67
+ throw new HttpError(400, code, issue?.message ?? 'Invalid record phrase request.', parseResult.error.issues.map((i) => ({
68
+ path: i.path.join('.') || '(root)',
69
+ issue: i.message,
70
+ })));
71
+ }
72
+ // The resolver middleware (kernel) fills the request scope's channel
73
+ // slot. Storefront paths fall back to the system-default channel; admin
74
+ // paths refuse with MISSING_SALES_CHANNEL_CONTEXT before reaching this
75
+ // handler.
76
+ const salesChannelId = currentSalesChannel()?.id;
77
+ if (!salesChannelId) {
78
+ throw new HttpError(400, ERROR_CODES.MISSING_SALES_CHANNEL_CONTEXT, 'No sales channel could be resolved for this request.');
79
+ }
80
+ // Fire-and-forget: the recorder swallows its own errors and the
81
+ // storefront response is not delayed by persistence (FR-015).
82
+ void phraseRecorder.record({
83
+ phrase: parseResult.data.phrase,
84
+ salesChannelId,
85
+ resultCount: parseResult.data.resultCount ?? 0,
86
+ });
87
+ return reply.code(202).send({ ok: true });
88
+ });
89
+ }
90
+ // GET /api/v1/search/suggest
91
+ app.get('/api/v1/search/suggest', async (request, reply) => {
92
+ const parsed = parseSuggestQuery(request);
93
+ const ctx = readContext(request);
94
+ // The popup has carried the buyer's own resolved price since feature 075
95
+ // and said nothing about it to a cache. It is the same private response the
96
+ // result feed below it now returns, so it gets the same header; the
97
+ // storefront's own typeahead fetch already asks for `no-store`.
98
+ markPersonalisedPricing(reply, ctx.audience);
99
+ try {
100
+ const result = await suggestService.suggest({
101
+ q: parsed.q,
102
+ ...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
103
+ }, ctx);
104
+ // Enrich with the per-customer price-list resolution when wired. The
105
+ // organization comes from the request actor (the suggest fetch carries
106
+ // the session cookie), so a signed-in buyer sees their negotiated price
107
+ // and a guest sees the public one. Enrichment failures degrade to the
108
+ // raw projection — the popup must never 500 over a pricing hiccup.
109
+ if (enrichSuggestionPricing) {
110
+ const organizationId = customerOrganizationId(request);
111
+ try {
112
+ const enriched = await enrichSuggestionPricing(result.data, {
113
+ resolvedChannel: ctx.resolvedChannel,
114
+ organizationId,
115
+ });
116
+ return {
117
+ data: enriched,
118
+ meta: result.meta,
119
+ };
120
+ }
121
+ catch {
122
+ // fall through to the un-enriched response
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ catch (err) {
128
+ if (err instanceof QueryTooShort) {
129
+ throw new HttpError(400, ERROR_CODES.QUERY_TOO_SHORT, `Query is shorter than the minimum length of ${err.minimumQueryLength} characters.`, [{ path: 'q', issue: 'phrase below minimum length' }]);
130
+ }
131
+ if (err instanceof SearchBackendUnavailable) {
132
+ throw new HttpError(503, ERROR_CODES.SEARCH_BACKEND_UNAVAILABLE, 'Search is temporarily unavailable.');
133
+ }
134
+ throw err;
135
+ }
136
+ });
137
+ }
138
+ /**
139
+ * Parses + validates `?q=…&limit=N`. Returns the canonicalised pair or
140
+ * raises an HttpError that the route handler does NOT need to translate.
141
+ *
142
+ * We hand-parse instead of letting Zod's `safeParse` populate the body
143
+ * because we want the search-specific error codes (`QUERY_TOO_LONG`,
144
+ * `LIMIT_OUT_OF_RANGE`) rather than the generic `VALIDATION_FAILED`.
145
+ */
146
+ function parseSuggestQuery(request) {
147
+ const raw = (request.query ?? {});
148
+ const qRaw = typeof raw['q'] === 'string' ? raw['q'] : undefined;
149
+ const limitRaw = typeof raw['limit'] === 'string' ? Number(raw['limit']) : undefined;
150
+ if (qRaw === undefined || qRaw.length === 0) {
151
+ throw new HttpError(400, ERROR_CODES.QUERY_TOO_SHORT, 'Query parameter "q" is required.', [{ path: 'q', issue: 'missing' }]);
152
+ }
153
+ if (qRaw.length > SEARCH_PHRASE_MAX_LENGTH) {
154
+ throw new HttpError(400, ERROR_CODES.QUERY_TOO_LONG, `Query exceeds the maximum length of ${SEARCH_PHRASE_MAX_LENGTH} characters.`, [{ path: 'q', issue: 'phrase too long' }]);
155
+ }
156
+ let limit;
157
+ if (limitRaw !== undefined) {
158
+ if (!Number.isInteger(limitRaw) ||
159
+ limitRaw <= 0 ||
160
+ limitRaw > SEARCH_SUGGEST_LIMIT_MAX) {
161
+ throw new HttpError(400, ERROR_CODES.LIMIT_OUT_OF_RANGE, `Limit must be an integer between 1 and ${SEARCH_SUGGEST_LIMIT_MAX}.`, [{ path: 'limit', issue: 'out of range' }]);
162
+ }
163
+ limit = limitRaw;
164
+ }
165
+ // Round-trip through the contract schema so the type system knows the
166
+ // values match the wire shape exactly. The schema mirrors the manual
167
+ // checks above, so this never throws — but it would catch any future
168
+ // drift between the route and the contract.
169
+ SearchSuggestQuerySchema.parse({ q: qRaw, ...(limit !== undefined ? { limit } : {}) });
170
+ return { q: qRaw, ...(limit !== undefined ? { limit } : {}) };
171
+ }
172
+ function readContext(request) {
173
+ // Feature 053 / FR-002: read the channel resolved once by the canonical
174
+ // middleware instead of re-parsing the `x-sales-channel` header.
175
+ const ch = getResolvedChannel(request);
176
+ const acceptLanguage = acceptLanguageHeaderSchema.parse(request.headers['accept-language']);
177
+ const preferredLanguage = acceptLanguage
178
+ ? acceptLanguage.split(',')[0]?.trim()
179
+ : undefined;
180
+ return {
181
+ resolvedChannel: {
182
+ id: ch.id,
183
+ code: ch.code,
184
+ isPublic: ch.isPublic,
185
+ defaultCurrency: ch.defaultCurrency,
186
+ defaultLanguage: ch.defaultLanguage,
187
+ },
188
+ audience: productAudienceOf(request),
189
+ ...(preferredLanguage !== undefined ? { preferredLanguage } : {}),
190
+ };
191
+ }
192
+ //# sourceMappingURL=routes.public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { ResolveResult } from '@endora-commerce/contracts';
2
+ import { type SettingsReadPort } from '@endora-commerce/platform/kernel';
3
+ /** Narrow port over CredentialsService.resolve (feature 058, Principle I). */
4
+ export interface CredentialResolvePort {
5
+ resolve(configurationCode: string): Promise<ResolveResult>;
6
+ }
7
+ export interface EmbedderConfig {
8
+ url: string;
9
+ apiKey: string;
10
+ model: string;
11
+ }
12
+ /**
13
+ * Resolve the Meilisearch embedder config for a channel (feature 058, T060).
14
+ *
15
+ * The `search.llm.embedder_credentials` reference (a reusable `llm` credential
16
+ * configuration) is the SINGLE source: its `baseUrl` → embedder URL, `apiKey` →
17
+ * embedder API key, `model` → embedder model. When the reference is unset or
18
+ * unresolvable, every field comes back empty and the caller treats LLM search as
19
+ * not configured (fail closed). There are no separate embedder settings.
20
+ *
21
+ * The secret `apiKey` is decrypted in-memory by `resolve`; it never leaves the
22
+ * process.
23
+ */
24
+ export declare function resolveEmbedderConfig(settings: SettingsReadPort, channelId: string, credentials?: CredentialResolvePort): Promise<EmbedderConfig>;
25
+ //# sourceMappingURL=embedder-config-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedder-config-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/embedder-config-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAGlG,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,qBAAqB,GAClC,OAAO,CAAC,cAAc,CAAC,CAmCzB"}