@open-mercato/shared 0.6.8-develop.7099.1.7ba2771d06 → 0.7.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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -4
- package/dist/lib/auth/jwt.js +0 -6
- package/dist/lib/auth/jwt.js.map +2 -2
- package/dist/lib/auth/organizationAccess.js +4 -7
- package/dist/lib/auth/organizationAccess.js.map +2 -2
- package/dist/lib/auth/server.js +7 -38
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/commands/command-bus.js +1 -6
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/crud/factory.js +8 -24
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/data/engine.js +2 -8
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/location/countries.js +0 -12
- package/dist/lib/location/countries.js.map +2 -2
- package/dist/lib/openapi/crud.js +1 -4
- package/dist/lib/openapi/crud.js.map +2 -2
- package/dist/lib/query/engine.js +34 -270
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/search/config.js +0 -1
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +15 -69
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js +0 -15
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/component-registry.js.map +2 -2
- package/package.json +3 -10
- package/src/lib/auth/__tests__/jwt.test.ts +0 -13
- package/src/lib/auth/__tests__/organizationAccess.test.ts +1 -36
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +0 -324
- package/src/lib/auth/__tests__/server.test.ts +0 -104
- package/src/lib/auth/jwt.ts +0 -17
- package/src/lib/auth/organizationAccess.ts +3 -11
- package/src/lib/auth/server.ts +8 -78
- package/src/lib/commands/__tests__/command-bus.test.ts +0 -31
- package/src/lib/commands/command-bus.ts +1 -8
- package/src/lib/crud/__tests__/crud-factory.test.ts +0 -165
- package/src/lib/crud/factory.ts +7 -33
- package/src/lib/data/__tests__/engine.event-validation.test.ts +1 -9
- package/src/lib/data/engine.ts +1 -7
- package/src/lib/location/__tests__/countries.test.ts +0 -15
- package/src/lib/location/countries.ts +0 -17
- package/src/lib/openapi/crud.ts +0 -3
- package/src/lib/query/__tests__/engine.count-distinct.test.ts +15 -162
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +1 -11
- package/src/lib/query/__tests__/engine.test.ts +7 -445
- package/src/lib/query/engine.ts +54 -434
- package/src/lib/query/types.ts +0 -15
- package/src/lib/search/config.ts +0 -10
- package/src/modules/events/factory.ts +19 -111
- package/src/modules/events/types.ts +0 -17
- package/src/modules/registry.ts +0 -40
- package/src/modules/widgets/component-registry.ts +0 -14
- package/dist/lib/auth/mfaPendingAccess.js +0 -42
- package/dist/lib/auth/mfaPendingAccess.js.map +0 -7
- package/dist/lib/auth/principal-service.js +0 -1
- package/dist/lib/auth/principal-service.js.map +0 -7
- package/dist/lib/html/htmlToPlainText.js +0 -16
- package/dist/lib/html/htmlToPlainText.js.map +0 -7
- package/dist/lib/query/count-cap.js +0 -11
- package/dist/lib/query/count-cap.js.map +0 -7
- package/dist/lib/queue/dispatchOrigin.js +0 -20
- package/dist/lib/queue/dispatchOrigin.js.map +0 -7
- package/dist/lib/search/entityAccess.js +0 -44
- package/dist/lib/search/entityAccess.js.map +0 -7
- package/src/lib/auth/__tests__/mfaPendingAccess.test.ts +0 -69
- package/src/lib/auth/__tests__/principalServiceExport.test.ts +0 -67
- package/src/lib/auth/mfaPendingAccess.ts +0 -70
- package/src/lib/auth/principal-service.ts +0 -110
- package/src/lib/html/__tests__/htmlToPlainText.test.ts +0 -59
- package/src/lib/html/htmlToPlainText.ts +0 -17
- package/src/lib/query/__tests__/count-cap-plan.test.ts +0 -240
- package/src/lib/query/__tests__/count-cap.test.ts +0 -41
- package/src/lib/query/count-cap.ts +0 -19
- package/src/lib/queue/dispatchOrigin.ts +0 -35
- package/src/lib/search/entityAccess.ts +0 -132
- package/src/modules/events/__tests__/factory.test.ts +0 -88
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import type { SearchEntityConfig } from '../../modules/search'
|
|
2
|
-
import { authorizeFeatures } from '../../security/featurePolicy'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Minimal shape of the `searchIndexer` DI service consumed by per-entity ACL
|
|
6
|
-
* resolution. Kept structural so callers and tests can pass a plain object
|
|
7
|
-
* instead of constructing a full `SearchIndexer`.
|
|
8
|
-
*/
|
|
9
|
-
export type SearchEntityConfigLookup = {
|
|
10
|
-
getEntityConfig: (entityId: string) => SearchEntityConfig | undefined
|
|
11
|
-
getAllEntityConfigs: () => SearchEntityConfig[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type SearchEntityAccessSubject = {
|
|
15
|
-
grantedFeatures: readonly string[]
|
|
16
|
-
isSuperAdmin?: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type SearchEntityDenyReason =
|
|
20
|
-
/** No module declares this entity in a `search.ts` config. */
|
|
21
|
-
| 'unconfigured'
|
|
22
|
-
/** The entity is configured for search but declares no `aclFeatures`. */
|
|
23
|
-
| 'no-acl-features'
|
|
24
|
-
/** The caller does not hold the entity's declared view feature(s). */
|
|
25
|
-
| 'insufficient-features'
|
|
26
|
-
|
|
27
|
-
export type SearchEntityAccessOptions = {
|
|
28
|
-
/**
|
|
29
|
-
* Called once per denied entity type. Exists so a silent drop is diagnosable:
|
|
30
|
-
* results disappearing because a module forgot to declare `aclFeatures` looks
|
|
31
|
-
* identical, from the palette, to results that simply did not match.
|
|
32
|
-
*/
|
|
33
|
-
onDeny?: (entityId: string, reason: SearchEntityDenyReason) => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Decide whether a caller may see results for one entity type.
|
|
38
|
-
*
|
|
39
|
-
* The single `search.global` gate on the palette only says "this user may use
|
|
40
|
-
* global search"; it says nothing about which records they may read. Each entity
|
|
41
|
-
* declares the owning module's view feature(s) in `aclFeatures`, and those are
|
|
42
|
-
* what actually authorize the read — the same rule the `search_get` /
|
|
43
|
-
* `search_aggregate` AI tools already apply.
|
|
44
|
-
*
|
|
45
|
-
* Fails closed: an entity that is not registered for search, or that declares no
|
|
46
|
-
* `aclFeatures`, is never exposed to a non-superadmin caller.
|
|
47
|
-
*/
|
|
48
|
-
export function canReadSearchEntity(
|
|
49
|
-
entityId: string,
|
|
50
|
-
lookup: SearchEntityConfigLookup,
|
|
51
|
-
subject: SearchEntityAccessSubject,
|
|
52
|
-
options: SearchEntityAccessOptions = {},
|
|
53
|
-
): boolean {
|
|
54
|
-
if (subject.isSuperAdmin) return true
|
|
55
|
-
|
|
56
|
-
const config = lookup.getEntityConfig(entityId)
|
|
57
|
-
if (!config) {
|
|
58
|
-
options.onDeny?.(entityId, 'unconfigured')
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const required = config.aclFeatures
|
|
63
|
-
if (!required || required.length === 0) {
|
|
64
|
-
options.onDeny?.(entityId, 'no-acl-features')
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const allowed = authorizeFeatures(required, {
|
|
69
|
-
grantedFeatures: subject.grantedFeatures,
|
|
70
|
-
unrestricted: false,
|
|
71
|
-
})
|
|
72
|
-
if (!allowed) options.onDeny?.(entityId, 'insufficient-features')
|
|
73
|
-
return allowed
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The entity types this caller may read, narrowed to `requestedEntityTypes` when
|
|
78
|
-
* the caller asked for specific ones.
|
|
79
|
-
*
|
|
80
|
-
* Restricting the query up front is what keeps `limit` meaningful. Filtering only
|
|
81
|
-
* after the search would spend the whole result budget on records the caller
|
|
82
|
-
* cannot see: an employee granted just `customers.people.view` would get the top
|
|
83
|
-
* 50 hits across every entity type, then watch most of them be dropped, and the
|
|
84
|
-
* palette would look empty even with hundreds of matching people behind it.
|
|
85
|
-
*
|
|
86
|
-
* Returns `undefined` when no restriction applies (superadmin with no explicit
|
|
87
|
-
* request), and an empty array when nothing is readable — callers should
|
|
88
|
-
* short-circuit on that rather than pass it down as "no filter".
|
|
89
|
-
*/
|
|
90
|
-
export function resolveReadableEntityTypes(
|
|
91
|
-
lookup: SearchEntityConfigLookup,
|
|
92
|
-
subject: SearchEntityAccessSubject,
|
|
93
|
-
requestedEntityTypes?: string[],
|
|
94
|
-
): string[] | undefined {
|
|
95
|
-
if (subject.isSuperAdmin) return requestedEntityTypes
|
|
96
|
-
|
|
97
|
-
const readable = lookup
|
|
98
|
-
.getAllEntityConfigs()
|
|
99
|
-
.filter((config) => config.enabled !== false)
|
|
100
|
-
.map((config) => config.entityId)
|
|
101
|
-
.filter((entityId) => canReadSearchEntity(entityId, lookup, subject))
|
|
102
|
-
|
|
103
|
-
if (!requestedEntityTypes) return readable
|
|
104
|
-
const requested = new Set(requestedEntityTypes)
|
|
105
|
-
return readable.filter((entityId) => requested.has(entityId))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Drop the results whose entity type the caller is not allowed to read.
|
|
110
|
-
*
|
|
111
|
-
* Filtering happens server-side so an under-privileged caller never receives the
|
|
112
|
-
* presenter title, subtitle or deep link of a record they cannot open. Decisions
|
|
113
|
-
* are memoized per entity type because a single response commonly mixes dozens of
|
|
114
|
-
* results across a handful of types.
|
|
115
|
-
*/
|
|
116
|
-
export function filterSearchResultsByEntityAccess<T extends { entityId: string }>(
|
|
117
|
-
results: readonly T[],
|
|
118
|
-
lookup: SearchEntityConfigLookup,
|
|
119
|
-
subject: SearchEntityAccessSubject,
|
|
120
|
-
options: SearchEntityAccessOptions = {},
|
|
121
|
-
): T[] {
|
|
122
|
-
if (subject.isSuperAdmin) return [...results]
|
|
123
|
-
|
|
124
|
-
const decisions = new Map<string, boolean>()
|
|
125
|
-
return results.filter((result) => {
|
|
126
|
-
const cached = decisions.get(result.entityId)
|
|
127
|
-
if (cached !== undefined) return cached
|
|
128
|
-
const allowed = canReadSearchEntity(result.entityId, lookup, subject, options)
|
|
129
|
-
decisions.set(result.entityId, allowed)
|
|
130
|
-
return allowed
|
|
131
|
-
})
|
|
132
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
const GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'
|
|
2
|
-
|
|
3
|
-
type EventFactoryModule = typeof import('../factory')
|
|
4
|
-
|
|
5
|
-
describe('event definition registry', () => {
|
|
6
|
-
const globalScope = globalThis as Record<string, unknown>
|
|
7
|
-
const originalRegistry = globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
delete globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
afterAll(() => {
|
|
14
|
-
if (originalRegistry === undefined) {
|
|
15
|
-
delete globalScope[GLOBAL_EVENT_REGISTRY_KEY]
|
|
16
|
-
} else {
|
|
17
|
-
globalScope[GLOBAL_EVENT_REGISTRY_KEY] = originalRegistry
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('shares declarations and registered configs across isolated module instances', () => {
|
|
22
|
-
let firstInstance: EventFactoryModule | undefined
|
|
23
|
-
let secondInstance: EventFactoryModule | undefined
|
|
24
|
-
|
|
25
|
-
jest.isolateModules(() => {
|
|
26
|
-
firstInstance = require('../factory') as EventFactoryModule
|
|
27
|
-
const config = firstInstance.createModuleEvents({
|
|
28
|
-
moduleId: 'isolated_events_test',
|
|
29
|
-
events: [{
|
|
30
|
-
id: 'isolated_events_test.invalidated',
|
|
31
|
-
label: 'Invalidated',
|
|
32
|
-
crossProcessBroadcast: true,
|
|
33
|
-
}] as const,
|
|
34
|
-
})
|
|
35
|
-
firstInstance.registerEventModuleConfigs([config])
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
jest.isolateModules(() => {
|
|
39
|
-
secondInstance = require('../factory') as EventFactoryModule
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
expect(secondInstance).not.toBe(firstInstance)
|
|
43
|
-
expect(secondInstance?.isEventDeclared('isolated_events_test.invalidated')).toBe(true)
|
|
44
|
-
expect(secondInstance?.isCrossProcessBroadcastEvent('isolated_events_test.invalidated')).toBe(true)
|
|
45
|
-
expect(secondInstance?.getDeclaredEvents()).toEqual(expect.arrayContaining([
|
|
46
|
-
expect.objectContaining({
|
|
47
|
-
id: 'isolated_events_test.invalidated',
|
|
48
|
-
module: 'isolated_events_test',
|
|
49
|
-
}),
|
|
50
|
-
]))
|
|
51
|
-
expect(secondInstance?.getEventModuleConfigs()).toHaveLength(1)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('refreshes a module definition during HMR without duplicating its event id', () => {
|
|
55
|
-
let firstInstance: EventFactoryModule | undefined
|
|
56
|
-
let secondInstance: EventFactoryModule | undefined
|
|
57
|
-
|
|
58
|
-
jest.isolateModules(() => {
|
|
59
|
-
firstInstance = require('../factory') as EventFactoryModule
|
|
60
|
-
firstInstance.createModuleEvents({
|
|
61
|
-
moduleId: 'hmr_events_test',
|
|
62
|
-
events: [{ id: 'hmr_events_test.changed', label: 'Before' }] as const,
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
jest.isolateModules(() => {
|
|
67
|
-
secondInstance = require('../factory') as EventFactoryModule
|
|
68
|
-
secondInstance.createModuleEvents({
|
|
69
|
-
moduleId: 'hmr_events_test',
|
|
70
|
-
events: [{
|
|
71
|
-
id: 'hmr_events_test.changed',
|
|
72
|
-
label: 'After',
|
|
73
|
-
clientBroadcast: true,
|
|
74
|
-
}] as const,
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(firstInstance?.isBroadcastEvent('hmr_events_test.changed')).toBe(true)
|
|
79
|
-
expect(firstInstance?.getAllDeclaredEventIds()).toEqual(['hmr_events_test.changed'])
|
|
80
|
-
expect(firstInstance?.getDeclaredEvents()).toEqual([
|
|
81
|
-
expect.objectContaining({
|
|
82
|
-
id: 'hmr_events_test.changed',
|
|
83
|
-
label: 'After',
|
|
84
|
-
clientBroadcast: true,
|
|
85
|
-
}),
|
|
86
|
-
])
|
|
87
|
-
})
|
|
88
|
-
})
|