@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.
- package/dist/lib/data/engine.js +25 -1
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +38 -9
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/encryption/kms.js +41 -6
- package/dist/lib/encryption/kms.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +4 -5
- package/src/lib/data/__tests__/engine.custom-entity-storage-guard.test.ts +78 -0
- package/src/lib/data/engine.ts +40 -0
- package/src/lib/db/__tests__/mikro.test.ts +82 -0
- package/src/lib/db/mikro.ts +55 -16
- package/src/lib/encryption/__tests__/kms.test.ts +80 -0
- package/src/lib/encryption/kms.ts +55 -7
- package/src/lib/query/types.ts +10 -0
- package/src/modules/navigation/backendChrome.ts +9 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 —
|
|
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.
|
|
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.
|
|
323
|
+
this.markTransientFailure()
|
|
276
324
|
console.warn('⚠️ [encryption][kms] Vault write error', {
|
|
277
325
|
path,
|
|
278
326
|
error: (err as Error)?.message || String(err),
|
package/src/lib/query/types.ts
CHANGED
|
@@ -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
|
}
|