@open-mercato/shared 0.6.5-develop.5382.1.f542de69af → 0.6.5

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.
@@ -5,6 +5,7 @@ import { parseBooleanToken } from '../boolean'
5
5
  import { fetchWithTimeout, resolveTimeoutMs } from '../http/fetchWithTimeout'
6
6
 
7
7
  const DEFAULT_VAULT_REQUEST_TIMEOUT_MS = 1_000
8
+ const DEFAULT_VAULT_RECOVERY_COOLDOWN_MS = 30_000
8
9
 
9
10
  function resolveVaultRequestTimeoutMs(): number {
10
11
  const raw = process.env.VAULT_REQUEST_TIMEOUT_MS
@@ -12,6 +13,12 @@ function resolveVaultRequestTimeoutMs(): number {
12
13
  return resolveTimeoutMs(parsed, DEFAULT_VAULT_REQUEST_TIMEOUT_MS)
13
14
  }
14
15
 
16
+ function resolveVaultRecoveryCooldownMs(): number {
17
+ const raw = process.env.VAULT_RECOVERY_COOLDOWN_MS
18
+ const parsed = raw ? Number.parseInt(raw, 10) : undefined
19
+ return resolveTimeoutMs(parsed, DEFAULT_VAULT_RECOVERY_COOLDOWN_MS)
20
+ }
21
+
15
22
  export type TenantDek = {
16
23
  tenantId: string
17
24
  key: string // base64
@@ -90,6 +97,7 @@ type VaultClientOpts = {
90
97
  mountPath?: string
91
98
  ttlMs?: number
92
99
  requestTimeoutMs?: number
100
+ recoveryCooldownMs?: number
93
101
  }
94
102
 
95
103
  type VaultReadResponse = {
@@ -166,7 +174,16 @@ export class HashicorpVaultKmsService implements KmsService {
166
174
  private readonly mountPath: string
167
175
  private readonly ttlMs: number
168
176
  private readonly requestTimeoutMs: number
177
+ private readonly recoveryCooldownMs: number
169
178
  private healthy = true
179
+ // Sticky terminal failure (missing VAULT_ADDR/VAULT_TOKEN): no amount of
180
+ // re-probing fixes a misconfiguration, so this never self-heals — only a
181
+ // restart with corrected config does.
182
+ private misconfigured = false
183
+ // Timestamp of the last transient failure (timeout / network blip / 5xx).
184
+ // Drives the half-open circuit breaker in isHealthy(): after the cooldown the
185
+ // instance reports healthy again so the next call re-probes Vault.
186
+ private lastTransientFailureAt: number | null = null
170
187
  private readonly debugEnabled: boolean
171
188
  private static loggedInit = false
172
189
 
@@ -176,9 +193,11 @@ export class HashicorpVaultKmsService implements KmsService {
176
193
  this.mountPath = (opts.mountPath || process.env.VAULT_KV_PATH || 'secret/data').replace(/\/+$/, '')
177
194
  this.ttlMs = opts.ttlMs ?? 15 * 60 * 1000
178
195
  this.requestTimeoutMs = resolveTimeoutMs(opts.requestTimeoutMs, resolveVaultRequestTimeoutMs())
196
+ this.recoveryCooldownMs = resolveTimeoutMs(opts.recoveryCooldownMs, resolveVaultRecoveryCooldownMs())
179
197
  this.debugEnabled = isEncryptionDebugEnabled()
180
198
  if (!this.vaultAddr || !this.vaultToken) {
181
199
  this.healthy = false
200
+ this.misconfigured = true
182
201
  if (this.debugEnabled) {
183
202
  console.warn('⚠️ [encryption][kms] Vault misconfigured (missing VAULT_ADDR or VAULT_TOKEN)')
184
203
  }
@@ -192,13 +211,34 @@ export class HashicorpVaultKmsService implements KmsService {
192
211
  }
193
212
 
194
213
  isHealthy(): boolean {
195
- return this.healthy
214
+ // A missing-config failure is terminal — never report healthy again.
215
+ if (this.misconfigured) return false
216
+ if (this.healthy) return true
217
+ // Half-open circuit breaker: once the cooldown since the last transient
218
+ // failure has elapsed, report healthy so the next read/write re-probes
219
+ // Vault. A successful probe flips `healthy` back on; a failing one records a
220
+ // fresh failure timestamp and re-opens the breaker for another cooldown.
221
+ if (this.lastTransientFailureAt === null) return false
222
+ return this.now() - this.lastTransientFailureAt >= this.recoveryCooldownMs
196
223
  }
197
224
 
198
225
  private now(): number {
199
226
  return Date.now()
200
227
  }
201
228
 
229
+ // Vault responded successfully (or is provably reachable): close the breaker.
230
+ private markHealthy(): void {
231
+ this.healthy = true
232
+ this.lastTransientFailureAt = null
233
+ }
234
+
235
+ // Transient infra failure (timeout / network blip / 5xx): open the breaker and
236
+ // start the recovery cooldown so a later call can re-probe and self-heal.
237
+ private markTransientFailure(): void {
238
+ this.healthy = false
239
+ this.lastTransientFailureAt = this.now()
240
+ }
241
+
202
242
  private cacheHit(tenantId: string): TenantDek | null {
203
243
  const entry = this.cache.get(tenantId)
204
244
  if (!entry) return null
@@ -212,6 +252,7 @@ export class HashicorpVaultKmsService implements KmsService {
212
252
  private async readVault(path: string): Promise<VaultReadResponse | null> {
213
253
  if (!this.vaultAddr || !this.vaultToken) {
214
254
  this.healthy = false
255
+ this.misconfigured = true
215
256
  return null
216
257
  }
217
258
  try {
@@ -221,16 +262,21 @@ export class HashicorpVaultKmsService implements KmsService {
221
262
  timeoutMs: this.requestTimeoutMs,
222
263
  })
223
264
  if (!res.ok) {
224
- this.healthy = res.status < 500
265
+ // 5xx = Vault down/erroring (transient). <500 (auth/not-found/etc.) means
266
+ // Vault is reachable and answered, so keep it healthy — a 404 for a
267
+ // not-yet-created tenant DEK is the normal read-before-write path.
268
+ if (res.status >= 500) this.markTransientFailure()
269
+ else this.markHealthy()
225
270
  console.warn('⚠️ [encryption][kms] Vault read failed', { path, status: res.status })
226
271
  return null
227
272
  }
273
+ this.markHealthy()
228
274
  if (this.debugEnabled) {
229
275
  console.info('🔍 [encryption][kms] Vault read ok', { path })
230
276
  }
231
277
  return (await res.json()) as VaultReadResponse
232
278
  } catch (err) {
233
- this.healthy = false
279
+ this.markTransientFailure()
234
280
  console.warn('⚠️ [encryption][kms] Vault read error', {
235
281
  path,
236
282
  error: (err as Error)?.message || String(err),
@@ -243,6 +289,7 @@ export class HashicorpVaultKmsService implements KmsService {
243
289
  private async writeVault(path: string, key: string, opts?: { cas?: number }): Promise<VaultWriteOutcome> {
244
290
  if (!this.vaultAddr || !this.vaultToken) {
245
291
  this.healthy = false
292
+ this.misconfigured = true
246
293
  return 'error'
247
294
  }
248
295
  const body: { data: { key: string }; options?: { cas: number } } = { data: { key } }
@@ -258,21 +305,22 @@ export class HashicorpVaultKmsService implements KmsService {
258
305
  timeoutMs: this.requestTimeoutMs,
259
306
  })
260
307
  if (res.ok) {
261
- this.healthy = true
308
+ this.markHealthy()
262
309
  return 'ok'
263
310
  }
264
311
  // KV v2 returns 400 when a check-and-set write loses to a concurrent
265
312
  // writer (path already at a newer version). That is a normal race outcome,
266
- // not an unhealthy Vault — don't flip `healthy`.
313
+ // not an unhealthy Vault — Vault is reachable, so close the breaker.
267
314
  if (typeof opts?.cas === 'number' && res.status === 400) {
315
+ this.markHealthy()
268
316
  console.warn('⚠️ [encryption][kms] Vault write CAS conflict (concurrent DEK create)', { path, status: res.status })
269
317
  return 'conflict'
270
318
  }
271
- this.healthy = false
319
+ this.markTransientFailure()
272
320
  console.warn('⚠️ [encryption][kms] Vault write failed', { path, status: res.status })
273
321
  return 'error'
274
322
  } catch (err) {
275
- this.healthy = false
323
+ this.markTransientFailure()
276
324
  console.warn('⚠️ [encryption][kms] Vault write error', {
277
325
  path,
278
326
  error: (err as Error)?.message || String(err),
@@ -125,6 +125,16 @@ export type QueryOptions = {
125
125
  // Used by the search indexing pipeline to prevent feedback loops where indexing triggers
126
126
  // re-indexing indefinitely.
127
127
  skipAutoReindex?: boolean
128
+ /**
129
+ * Force routing this query to custom-entity doc storage (`custom_entities_storage`)
130
+ * instead of classifying the entity automatically. Automatic classification routes
131
+ * ids backed by a registered ORM table to that base table, so surfaces that manage
132
+ * doc records for ids that are ALSO table-backed (e.g. the entities records browser
133
+ * reading a module-declared custom entity such as `example:todo`) must set this flag.
134
+ * Honored by the hybrid query engine only; `BasicQueryEngine` has no doc-storage
135
+ * reader and ignores it.
136
+ */
137
+ forceCustomEntityStorage?: boolean
128
138
  // Optional UMES query extensions context. When provided, the engine will
129
139
  // emit sync lifecycle events and apply query-level enrichers.
130
140
  extensions?: QueryExtensionsConfig
@@ -39,6 +39,14 @@ export type BackendChromeSectionGroup = {
39
39
  order?: number
40
40
  }
41
41
 
42
+ export type BackendChromeBrand = {
43
+ name?: string
44
+ logo?: {
45
+ src: string
46
+ alt?: string
47
+ } | null
48
+ }
49
+
42
50
  export type BackendChromePayload = {
43
51
  groups: BackendChromeNavGroup[]
44
52
  settingsSections: BackendChromeSectionGroup[]
@@ -47,4 +55,5 @@ export type BackendChromePayload = {
47
55
  profilePathPrefixes: string[]
48
56
  grantedFeatures: string[]
49
57
  roles: string[]
58
+ brand?: BackendChromeBrand | null
50
59
  }