@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.
Files changed (81) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -4
  3. package/dist/lib/auth/jwt.js +0 -6
  4. package/dist/lib/auth/jwt.js.map +2 -2
  5. package/dist/lib/auth/organizationAccess.js +4 -7
  6. package/dist/lib/auth/organizationAccess.js.map +2 -2
  7. package/dist/lib/auth/server.js +7 -38
  8. package/dist/lib/auth/server.js.map +2 -2
  9. package/dist/lib/commands/command-bus.js +1 -6
  10. package/dist/lib/commands/command-bus.js.map +2 -2
  11. package/dist/lib/crud/factory.js +8 -24
  12. package/dist/lib/crud/factory.js.map +2 -2
  13. package/dist/lib/data/engine.js +2 -8
  14. package/dist/lib/data/engine.js.map +2 -2
  15. package/dist/lib/location/countries.js +0 -12
  16. package/dist/lib/location/countries.js.map +2 -2
  17. package/dist/lib/openapi/crud.js +1 -4
  18. package/dist/lib/openapi/crud.js.map +2 -2
  19. package/dist/lib/query/engine.js +34 -270
  20. package/dist/lib/query/engine.js.map +3 -3
  21. package/dist/lib/query/types.js.map +1 -1
  22. package/dist/lib/search/config.js +0 -1
  23. package/dist/lib/search/config.js.map +2 -2
  24. package/dist/lib/version.js +1 -1
  25. package/dist/lib/version.js.map +1 -1
  26. package/dist/modules/events/factory.js +15 -69
  27. package/dist/modules/events/factory.js.map +2 -2
  28. package/dist/modules/registry.js +0 -15
  29. package/dist/modules/registry.js.map +2 -2
  30. package/dist/modules/widgets/component-registry.js.map +2 -2
  31. package/package.json +3 -10
  32. package/src/lib/auth/__tests__/jwt.test.ts +0 -13
  33. package/src/lib/auth/__tests__/organizationAccess.test.ts +1 -36
  34. package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +0 -324
  35. package/src/lib/auth/__tests__/server.test.ts +0 -104
  36. package/src/lib/auth/jwt.ts +0 -17
  37. package/src/lib/auth/organizationAccess.ts +3 -11
  38. package/src/lib/auth/server.ts +8 -78
  39. package/src/lib/commands/__tests__/command-bus.test.ts +0 -31
  40. package/src/lib/commands/command-bus.ts +1 -8
  41. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -165
  42. package/src/lib/crud/factory.ts +7 -33
  43. package/src/lib/data/__tests__/engine.event-validation.test.ts +1 -9
  44. package/src/lib/data/engine.ts +1 -7
  45. package/src/lib/location/__tests__/countries.test.ts +0 -15
  46. package/src/lib/location/countries.ts +0 -17
  47. package/src/lib/openapi/crud.ts +0 -3
  48. package/src/lib/query/__tests__/engine.count-distinct.test.ts +15 -162
  49. package/src/lib/query/__tests__/engine.scope-and-or.test.ts +1 -11
  50. package/src/lib/query/__tests__/engine.test.ts +7 -445
  51. package/src/lib/query/engine.ts +54 -434
  52. package/src/lib/query/types.ts +0 -15
  53. package/src/lib/search/config.ts +0 -10
  54. package/src/modules/events/factory.ts +19 -111
  55. package/src/modules/events/types.ts +0 -17
  56. package/src/modules/registry.ts +0 -40
  57. package/src/modules/widgets/component-registry.ts +0 -14
  58. package/dist/lib/auth/mfaPendingAccess.js +0 -42
  59. package/dist/lib/auth/mfaPendingAccess.js.map +0 -7
  60. package/dist/lib/auth/principal-service.js +0 -1
  61. package/dist/lib/auth/principal-service.js.map +0 -7
  62. package/dist/lib/html/htmlToPlainText.js +0 -16
  63. package/dist/lib/html/htmlToPlainText.js.map +0 -7
  64. package/dist/lib/query/count-cap.js +0 -11
  65. package/dist/lib/query/count-cap.js.map +0 -7
  66. package/dist/lib/queue/dispatchOrigin.js +0 -20
  67. package/dist/lib/queue/dispatchOrigin.js.map +0 -7
  68. package/dist/lib/search/entityAccess.js +0 -44
  69. package/dist/lib/search/entityAccess.js.map +0 -7
  70. package/src/lib/auth/__tests__/mfaPendingAccess.test.ts +0 -69
  71. package/src/lib/auth/__tests__/principalServiceExport.test.ts +0 -67
  72. package/src/lib/auth/mfaPendingAccess.ts +0 -70
  73. package/src/lib/auth/principal-service.ts +0 -110
  74. package/src/lib/html/__tests__/htmlToPlainText.test.ts +0 -59
  75. package/src/lib/html/htmlToPlainText.ts +0 -17
  76. package/src/lib/query/__tests__/count-cap-plan.test.ts +0 -240
  77. package/src/lib/query/__tests__/count-cap.test.ts +0 -41
  78. package/src/lib/query/count-cap.ts +0 -19
  79. package/src/lib/queue/dispatchOrigin.ts +0 -35
  80. package/src/lib/search/entityAccess.ts +0 -132
  81. 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
- })