@fougere/core 0.1.0-alpha.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/binding.d.ts +63 -0
  4. package/dist/binding.d.ts.map +1 -0
  5. package/dist/binding.js +107 -0
  6. package/dist/binding.js.map +1 -0
  7. package/dist/boot.d.ts +38 -0
  8. package/dist/boot.d.ts.map +1 -0
  9. package/dist/boot.js +53 -0
  10. package/dist/boot.js.map +1 -0
  11. package/dist/bootstrap.d.ts +4 -0
  12. package/dist/bootstrap.d.ts.map +1 -0
  13. package/dist/bootstrap.js +825 -0
  14. package/dist/bootstrap.js.map +1 -0
  15. package/dist/builtins/config.d.ts +4 -0
  16. package/dist/builtins/config.d.ts.map +1 -0
  17. package/dist/builtins/config.js +6 -0
  18. package/dist/builtins/config.js.map +1 -0
  19. package/dist/builtins/logger.d.ts +28 -0
  20. package/dist/builtins/logger.d.ts.map +1 -0
  21. package/dist/builtins/logger.js +76 -0
  22. package/dist/builtins/logger.js.map +1 -0
  23. package/dist/call.d.ts +172 -0
  24. package/dist/call.d.ts.map +1 -0
  25. package/dist/call.js +209 -0
  26. package/dist/call.js.map +1 -0
  27. package/dist/collector.d.ts +28 -0
  28. package/dist/collector.d.ts.map +1 -0
  29. package/dist/collector.js +32 -0
  30. package/dist/collector.js.map +1 -0
  31. package/dist/config-loader.d.ts +59 -0
  32. package/dist/config-loader.d.ts.map +1 -0
  33. package/dist/config-loader.js +97 -0
  34. package/dist/config-loader.js.map +1 -0
  35. package/dist/contract.d.ts +43 -0
  36. package/dist/contract.d.ts.map +1 -0
  37. package/dist/contract.js +29 -0
  38. package/dist/contract.js.map +1 -0
  39. package/dist/crud.d.ts +61 -0
  40. package/dist/crud.d.ts.map +1 -0
  41. package/dist/crud.js +85 -0
  42. package/dist/crud.js.map +1 -0
  43. package/dist/define.d.ts +8 -0
  44. package/dist/define.d.ts.map +1 -0
  45. package/dist/define.js +9 -0
  46. package/dist/define.js.map +1 -0
  47. package/dist/egress.d.ts +78 -0
  48. package/dist/egress.d.ts.map +1 -0
  49. package/dist/egress.js +185 -0
  50. package/dist/egress.js.map +1 -0
  51. package/dist/emit.d.ts +51 -0
  52. package/dist/emit.d.ts.map +1 -0
  53. package/dist/emit.js +57 -0
  54. package/dist/emit.js.map +1 -0
  55. package/dist/errors.d.ts +22 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +74 -0
  58. package/dist/errors.js.map +1 -0
  59. package/dist/frond-config.d.ts +80 -0
  60. package/dist/frond-config.d.ts.map +1 -0
  61. package/dist/frond-config.js +32 -0
  62. package/dist/frond-config.js.map +1 -0
  63. package/dist/graph.d.ts +33 -0
  64. package/dist/graph.d.ts.map +1 -0
  65. package/dist/graph.js +142 -0
  66. package/dist/graph.js.map +1 -0
  67. package/dist/handler-parser.d.ts +82 -0
  68. package/dist/handler-parser.d.ts.map +1 -0
  69. package/dist/handler-parser.js +505 -0
  70. package/dist/handler-parser.js.map +1 -0
  71. package/dist/imports.d.ts +22 -0
  72. package/dist/imports.d.ts.map +1 -0
  73. package/dist/imports.js +80 -0
  74. package/dist/imports.js.map +1 -0
  75. package/dist/index.d.ts +43 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +37 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/invocation.d.ts +20 -0
  80. package/dist/invocation.d.ts.map +1 -0
  81. package/dist/invocation.js +15 -0
  82. package/dist/invocation.js.map +1 -0
  83. package/dist/middleware/error-handler.d.ts +15 -0
  84. package/dist/middleware/error-handler.d.ts.map +1 -0
  85. package/dist/middleware/error-handler.js +29 -0
  86. package/dist/middleware/error-handler.js.map +1 -0
  87. package/dist/middleware/logger.d.ts +14 -0
  88. package/dist/middleware/logger.d.ts.map +1 -0
  89. package/dist/middleware/logger.js +25 -0
  90. package/dist/middleware/logger.js.map +1 -0
  91. package/dist/middleware.d.ts +75 -0
  92. package/dist/middleware.d.ts.map +1 -0
  93. package/dist/middleware.js +89 -0
  94. package/dist/middleware.js.map +1 -0
  95. package/dist/operation.d.ts +87 -0
  96. package/dist/operation.d.ts.map +1 -0
  97. package/dist/operation.js +95 -0
  98. package/dist/operation.js.map +1 -0
  99. package/dist/orm.d.ts +96 -0
  100. package/dist/orm.d.ts.map +1 -0
  101. package/dist/orm.js +33 -0
  102. package/dist/orm.js.map +1 -0
  103. package/dist/presenter.d.ts +63 -0
  104. package/dist/presenter.d.ts.map +1 -0
  105. package/dist/presenter.js +72 -0
  106. package/dist/presenter.js.map +1 -0
  107. package/dist/remote.d.ts +48 -0
  108. package/dist/remote.d.ts.map +1 -0
  109. package/dist/remote.js +135 -0
  110. package/dist/remote.js.map +1 -0
  111. package/dist/repository.d.ts +55 -0
  112. package/dist/repository.d.ts.map +1 -0
  113. package/dist/repository.js +26 -0
  114. package/dist/repository.js.map +1 -0
  115. package/dist/scan-cache.d.ts +10 -0
  116. package/dist/scan-cache.d.ts.map +1 -0
  117. package/dist/scan-cache.js +92 -0
  118. package/dist/scan-cache.js.map +1 -0
  119. package/dist/scanner.d.ts +34 -0
  120. package/dist/scanner.d.ts.map +1 -0
  121. package/dist/scanner.js +595 -0
  122. package/dist/scanner.js.map +1 -0
  123. package/dist/seed.d.ts +28 -0
  124. package/dist/seed.d.ts.map +1 -0
  125. package/dist/seed.js +110 -0
  126. package/dist/seed.js.map +1 -0
  127. package/dist/types.d.ts +387 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +2 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/verify.d.ts +57 -0
  132. package/dist/verify.d.ts.map +1 -0
  133. package/dist/verify.js +147 -0
  134. package/dist/verify.js.map +1 -0
  135. package/package.json +47 -0
@@ -0,0 +1,825 @@
1
+ import { runMiddlewares, FougereError, ErrorCode } from './middleware.js';
2
+ import { scanProject } from './scanner.js';
3
+ import { Logger } from './builtins/logger.js';
4
+ import { Config } from './builtins/config.js';
5
+ import { createRemoteRouter, createRemoteFacade } from './remote.js';
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+ import { facadeKeyOf } from './call.js';
8
+ import { emitKeyOf, factsAnnouncedBy } from './emit.js';
9
+ import { repositoryKeyOf } from './repository.js';
10
+ // The keys, each read from where its concept is declared — never respelled here.
11
+ import { ormKeyOf } from './orm.js';
12
+ import { presenterKeyOf } from './presenter.js';
13
+ import { collectorKeyOf } from './collector.js';
14
+ import { computeBindingPlan, resolveArgs } from './binding.js';
15
+ import { resolveContracts } from './operation.js';
16
+ import { EMPTY_INVOCATION } from './invocation.js';
17
+ import { validateFields, applyCreate } from '@fougere/schema';
18
+ import { projectEgress, presentEgress, guardStorage } from './egress.js';
19
+ /**
20
+ * The one wording for "nobody hosts this here", with both ways out. Said twice, and the
21
+ * second copy (`schemaFor`) had lost the two remedies — the same dead end, strictly less
22
+ * useful, for no reason.
23
+ */
24
+ const notLoaded = (entity) => `Frond for '${entity}' is not loaded.\n` +
25
+ ` - Add '${entity}' to --fronds flag\n` +
26
+ ` - Or declare a remote: remotes: { ${entity}: 'http://...' }`;
27
+ /**
28
+ * Two fronds cannot claim one name — said at boot, because nothing else says it.
29
+ *
30
+ * A door lands on the ROOT container under `facadeKeyOf(address)`, and that key carries
31
+ * no frond; a presenter lands there too. `registerValue` is a `Map.set`, so the second
32
+ * frond loaded simply replaced the first and every call meant for one went to the other.
33
+ * Silent in-process, and worse than silent under a split: `createRemoteRouter` guards its
34
+ * index with `if (!byEntity.has(...))`, so THERE the first frond discovered wins. The same
35
+ * application answered differently depending on how it was deployed.
36
+ *
37
+ * Refusing is the honest answer while a key cannot say which frond owns it. It is not the
38
+ * last word: qualify the key and this boot can accept both. ORMs and repositories are not
39
+ * checked because they are registered in the frond's own scope, where two fronds do not meet.
40
+ *
41
+ * A frond declared remote registers nothing locally, so it cannot collide here. Two REMOTE
42
+ * fronds publishing one entity still shadow each other inside the router — a hole this
43
+ * check does not reach.
44
+ */
45
+ function assertOneOwnerPerKey(fronds, remotes) {
46
+ const owner = new Map();
47
+ const claim = (key, frond, what) => {
48
+ const held = owner.get(key);
49
+ if (held !== undefined && held !== frond) {
50
+ throw new Error(`Two fronds claim the key '${key}': '${held}' and '${frond}'.\n`
51
+ + ` A ${what} is registered under a key that names no frond, so one would silently replace the other.\n`
52
+ + ` - Rename one of the two classes, or\n`
53
+ + ` - keep one of the two fronds out of this process (--fronds), or declare it in remotes:`);
54
+ }
55
+ owner.set(key, frond);
56
+ };
57
+ for (const frond of fronds) {
58
+ if (remotes && frond.name in remotes)
59
+ continue;
60
+ for (const handler of frond.handlers)
61
+ claim(facadeKeyOf(handler.address, handler.surface), frond.name, 'door');
62
+ for (const presenter of frond.presenters)
63
+ claim(presenterKeyOf(presenter.entityName), frond.name, 'presenter');
64
+ }
65
+ }
66
+ /** Bootstrap a fougere application. */
67
+ export async function createApp(options) {
68
+ const root = options.root ?? process.cwd();
69
+ const container = options.createContainer();
70
+ // Boot chatter is debug by default; a host (e.g. the CLI) can quiet it.
71
+ const log = new Logger('boot:app', { level: process.env.FOUGERE_LOG_LEVEL ?? 'debug' });
72
+ // Builtins — registered under class name (PascalCase) for type-based DI
73
+ container.registerValue('Logger', new Logger());
74
+ container.register('Config', Config, { lifetime: 'singleton' });
75
+ log.debug('builtins registered (Logger, Config)');
76
+ // Scan (with optional filter)
77
+ const scanStart = performance.now();
78
+ const { fronds, diagnostics } = await scanProject(root, options.fronds);
79
+ const scanMs = (performance.now() - scanStart).toFixed(0);
80
+ const blocking = diagnostics.filter((d) => d.severity === 'blocking');
81
+ log.info(`scanned ${fronds.length} frond(s) in ${scanMs}ms`
82
+ + (diagnostics.length ? ` — ${diagnostics.length} thing(s) the scan could not do` : ''));
83
+ /**
84
+ * Say what could not be read, at the one line everyone already watches.
85
+ *
86
+ * Not a refusal: an app whose `presenters/` is unreadable still serves its
87
+ * entities, and stopping the boot would trade a partial app for none. But it is
88
+ * an ERROR, not a debug line — the app now serves less than its source declares,
89
+ * and nothing downstream can tell that from a source that declares less.
90
+ */
91
+ for (const d of blocking)
92
+ log.error(`[${d.code}] ${d.message}`, d.cause);
93
+ for (const d of diagnostics)
94
+ if (d.severity === 'warning')
95
+ log.warn(`[${d.code}] ${d.message}`);
96
+ // Auth runtime — built once from the lazy AuthConfig produced by a provider factory
97
+ // (e.g. betterAuth({...})) in fougere.config.ts. The provider receives our db +
98
+ // ormFactory so all auth writes flow through EntityOrm.
99
+ let authRuntime;
100
+ if (options.auth) {
101
+ if (!options.ormFactory) {
102
+ throw new Error('createApp: `auth` is set but `ormFactory` is missing — auth providers need it to back their adapter.');
103
+ }
104
+ if (options.db === undefined) {
105
+ throw new Error('createApp: `auth` is set but `db` is missing — pass the storage handle through CreateAppOptions.db.');
106
+ }
107
+ log.info('initializing auth runtime');
108
+ authRuntime = await options.auth.create({
109
+ db: options.db,
110
+ ormFactory: options.ormFactory,
111
+ });
112
+ log.info(`auth ready — mounted at ${authRuntime.basePath}`);
113
+ }
114
+ // Remote routing — judged at boot: declaring remotes without a transport is a config error.
115
+ // A remote declaration wins over local presence: `remotes: { blog: url }` IS
116
+ // the topology statement — the frond's code may sit in fronds/**, it runs elsewhere.
117
+ const declaredRemotes = Object.entries(options.remotes ?? {});
118
+ if (declaredRemotes.length > 0 && !options.remoteTransport) {
119
+ throw new Error('createApp: `remotes` is declared but `remoteTransport` is missing — pass one (e.g. from @fougere/transport-http).');
120
+ }
121
+ const remoteRouter = declaredRemotes.length > 0 && options.remoteTransport
122
+ ? createRemoteRouter(Object.fromEntries(declaredRemotes), options.remoteTransport)
123
+ : undefined;
124
+ // Middleware storage — read at call time, not at boot time
125
+ const globalMiddlewares = [];
126
+ const scopedMiddlewares = new Map();
127
+ function getMiddlewares(entity) {
128
+ const scoped = scopedMiddlewares.get(entity) ?? [];
129
+ return [...globalMiddlewares, ...scoped];
130
+ }
131
+ assertOneOwnerPerKey(fronds, options.remotes);
132
+ /**
133
+ * Emissions — the only place in Fougere where an initiator names a SUBJECT.
134
+ *
135
+ * The set comes from the DEPS, not from the subscribers: a handler that declares
136
+ * `Emit<PostPublished>` must resolve it whether or not anybody listens, and announcing
137
+ * to nobody is legal. The index is filled by `buildFacade` as each contract is resolved,
138
+ * and the value below closes over it — so no order between the two ever matters.
139
+ */
140
+ const emitted = new Set(fronds.flatMap((frond) => factsAnnouncedBy(frond.handlers)));
141
+ const subscribers = new Map();
142
+ /**
143
+ * Every entity of every frond, by name — so a fact can be judged where it LANDS.
144
+ *
145
+ * A fact usually lives in the frond that announces it and is heard in another, so the
146
+ * subscriber's own frond does not hold it. Across all fronds and not per-frond for that
147
+ * one reason.
148
+ */
149
+ const entityByName = new Map(fronds.flatMap((f) => f.entities.map((e) => [e.name, e.entityClass])));
150
+ /**
151
+ * Who listens to what — read from the PLAN, where `{ kind: 'fact' }` is a sentence
152
+ * `computeBindingPlan` already wrote, so nothing re-derives what a parameter is.
153
+ *
154
+ * It runs for a frond hosted here AND for one declared remote. A remote frond is still
155
+ * scanned — only its hosting is elsewhere — so its subscriptions are known, and its door
156
+ * resolves to a doublure. That is the whole reason an emission crosses a process without
157
+ * a line of transport code: the emitter learned the signature locally and calls the same
158
+ * key. Filling this inside `buildFacade` alone left the index EMPTY under a split, and a
159
+ * fact announced to a remote listener reached nobody, in silence.
160
+ */
161
+ const noteSubscriptions = (contracts, door) => {
162
+ for (const [op, contract] of contracts) {
163
+ for (const bound of contract.binding ?? []) {
164
+ if (bound.source.kind !== 'fact')
165
+ continue;
166
+ const listeners = subscribers.get(bound.source.factName) ?? [];
167
+ listeners.push({ door, op });
168
+ subscribers.set(bound.source.factName, listeners);
169
+ }
170
+ }
171
+ };
172
+ /**
173
+ * The facts already being announced up the stack, so a fact cannot cause itself.
174
+ *
175
+ * A CHAIN and not a depth: `A → B → D` and `A → C → D` is a diamond, perfectly legal,
176
+ * while `A → … → A` never ends. Carried in async context because a nested emission
177
+ * happens inside a subscriber, whose own `Emit` closure never sees the invocation that
178
+ * reached it.
179
+ *
180
+ * Detecting this at boot was the first idea and it was wrong: `Emit<G>` is a CONSTRUCTOR
181
+ * dependency, so it belongs to the handler and not to one of its methods. A handler that
182
+ * subscribes to `A` in one method and emits `G` from another would have been refused for
183
+ * a cycle it never walks. Refusing a correct program is worse than a guard that costs
184
+ * one array per emission.
185
+ */
186
+ const chain = new AsyncLocalStorage();
187
+ // Register frond scopes
188
+ for (const frond of fronds) {
189
+ // Declared remote: keep the scanned metadata (bridges route with it),
190
+ // register nothing locally — resolve() falls through to the remote façade.
191
+ if (options.remotes && frond.name in options.remotes) {
192
+ log.child(frond.name).info('declared remote — not hosted locally');
193
+ // Its doors answer elsewhere, but what they LISTEN to was read here.
194
+ const remoteCollectors = new Set(frond.collectors.map((c) => c.entityName));
195
+ for (const handler of frond.handlers) {
196
+ noteSubscriptions(resolveContracts(handler, frond.operationsOverrides, remoteCollectors), facadeKeyOf(handler.address, handler.surface));
197
+ }
198
+ continue;
199
+ }
200
+ const scope = container.createScope();
201
+ const frondLog = log.child(frond.name);
202
+ for (const provider of frond.providers) {
203
+ scope.register(provider.ctor.name, provider.ctor, { deps: provider.deps });
204
+ }
205
+ if (frond.providers.length > 0) {
206
+ frondLog.debug(`${frond.providers.length} provider(s): ${frond.providers.map((p) => p.ctor.name).join(', ')}`);
207
+ }
208
+ // Register EntityOrm for each entity — PascalCase type name (e.g. 'PostOrm')
209
+ // When a handler declares Crud(Entity, Output), scope the ORM via .output(Output)
210
+ if (options.ormFactory) {
211
+ for (const entity of frond.entities) {
212
+ const ormName = ormKeyOf(entity.name);
213
+ const baseOrm = options.ormFactory(entity.entityClass, entity.name);
214
+ // Check if the default handler (no surface) declares an output override
215
+ const defaultHandler = frond.handlers.find((h) => h.address === entity.name && !h.surface);
216
+ const outputSchema = defaultHandler?.outputOverride ?? defaultHandler?.ctor?.__output;
217
+ const scoped = outputSchema && outputSchema !== entity.entityClass
218
+ ? baseOrm.output(outputSchema)
219
+ : baseOrm;
220
+ // Storage is a way out like the client surface — see egress.ts.
221
+ const guarded = guardStorage(scoped, entity.entityClass.getFields(), entity.name);
222
+ scope.registerValue(ormName, guarded);
223
+ // The default repository holds the port and adds nothing — the same shape a
224
+ // declared one has, so a handler reads `repo.orm.list()` either way. Giving
225
+ // the ORM itself as the default was shorter and wrong: `repo.orm` would then
226
+ // exist only when someone had written the file, and the two forms would
227
+ // differ exactly where the convention promises they do not.
228
+ //
229
+ // `providers` are registered above, so a declared repository is never
230
+ // overwritten by this.
231
+ const repoKey = repositoryKeyOf(entity.name);
232
+ if (!scope.has(repoKey))
233
+ scope.registerValue(repoKey, { orm: guarded });
234
+ }
235
+ if (frond.entities.length > 0) {
236
+ frondLog.debug(`${frond.entities.length} entity ORM(s): ${frond.entities.map((e) => e.name).join(', ')}`);
237
+ }
238
+ }
239
+ // Register presenters in scope — PascalCase type name (e.g. 'PostPresenter')
240
+ const presenterMap = new Map(frond.presenters.map((p) => [p.entityName, p]));
241
+ for (const presenter of frond.presenters) {
242
+ scope.register(presenterKeyOf(presenter.entityName), presenter.ctor, { deps: presenter.deps });
243
+ }
244
+ if (frond.presenters.length > 0) {
245
+ frondLog.debug(`${frond.presenters.length} presenter(s): ${frond.presenters.map((p) => p.entityName).join(', ')}`);
246
+ }
247
+ // Register collectors in scope — PascalCase type name (e.g. 'UserCollector')
248
+ const collectorEntityNames = new Set(frond.collectors.map((c) => c.entityName));
249
+ for (const collector of frond.collectors) {
250
+ const key = collectorKeyOf(collector.entityName);
251
+ scope.register(key, collector.ctor, { deps: collector.deps });
252
+ }
253
+ if (frond.collectors.length > 0) {
254
+ frondLog.debug(`${frond.collectors.length} collector(s): ${frond.collectors.map((c) => c.entityName).join(', ')}`);
255
+ }
256
+ // Build handler facades → registered in ROOT container (public contract)
257
+ const defaultHandlers = frond.handlers.filter((h) => !h.surface);
258
+ const surfaceHandlers = frond.handlers.filter((h) => h.surface);
259
+ const defaultHandlerMap = new Map(defaultHandlers.map((h) => [h.address, h]));
260
+ /**
261
+ * Build a facade for a handler and register it in the root container.
262
+ *
263
+ * A facade is built FROM a handler — that is the whole rule, and the entity is
264
+ * OPTIONAL. An entity is a shape, not a surface: on its own it declares no
265
+ * operation, so it gets no facade and answers nothing. The converse used to be
266
+ * false in fact though true on paper — the loop below walked entities, so a
267
+ * handler naming no entity was scanned and then silently never built. An
268
+ * operation that is about no stored row (`health.check`, a pure computation) is
269
+ * an ordinary case, not a gap to accommodate.
270
+ *
271
+ * Without an entity a facade loses exactly three things, and nothing else: the
272
+ * ORM injected by convention, the output fields to project onto, and the
273
+ * presenter. Its result travels as the handler returned it.
274
+ */
275
+ const buildFacade = (entity, handler, targetScope, facadeKey) => {
276
+ const handlerKey = `_handler:${facadeKey}`;
277
+ // The ORM belongs to the SUBJECT, never to the address. `StockHandler extends
278
+ // Crud(Item)` is called `stock` and reads `Item`; asking for `StockOrm` would be
279
+ // asking the address for a table. The two coincide in the ordinary case and that
280
+ // is why it went unnoticed.
281
+ const ormBase = entity?.name ?? handler.address;
282
+ const ormTypeName = ormKeyOf(ormBase);
283
+ const inheritsCrud = typeof handler.ctor.prototype?.list === 'function'
284
+ && typeof handler.ctor.prototype?.findById === 'function';
285
+ const hasCrudInProto = handler.deps.length === 0 && inheritsCrud;
286
+ const deps = handler.deps.length > 0
287
+ ? handler.deps
288
+ : hasCrudInProto ? [ormTypeName] : [];
289
+ // Declaring a constructor turns the automatic ORM injection OFF — the handler now
290
+ // states what it takes, and that is the whole DI convention. But a Crud handler
291
+ // that forgets to state its ORM used to get `this.orm === undefined` and break on
292
+ // the FIRST REQUEST, silently: `super()` assigns whatever it was handed. Refuse at
293
+ // boot instead, naming the fix — the clause is deducible, so it is stated, not
294
+ // configured.
295
+ if (inheritsCrud && handler.deps.length > 0 && !handler.deps.includes(ormTypeName)) {
296
+ throw new Error(`${handler.ctor.name} extends Crud() and declares a constructor, so the ORM is no ` +
297
+ `longer injected for it — but it does not take one.\n` +
298
+ ` Add it and hand it to super():\n` +
299
+ ` constructor(orm: ${ormTypeName}, …) { super(orm); }`);
300
+ }
301
+ // A Crud handler whose subject is not among the scanned entities is either broken
302
+ // or installed — `Crud(Note)` from a published package is legitimate and the scan
303
+ // cannot see it (CLAUDE.md, heritage resolution is workspace-only). Refusing at
304
+ // boot would break the second case to catch the first, so it is said, not thrown.
305
+ if (inheritsCrud && !entity) {
306
+ frondLog.debug(`${handler.ctor.name} extends Crud() and no scanned entity is named `
307
+ + `'${ormBase}' — installed entity, or a missing one: no ORM will be injected`);
308
+ }
309
+ targetScope.register(handlerKey, handler.ctor, { deps });
310
+ let instance;
311
+ const getInstance = () => {
312
+ if (!instance)
313
+ instance = targetScope.resolve(handlerKey);
314
+ return instance;
315
+ };
316
+ /**
317
+ * The contracts this façade serves — one function, shared with every other
318
+ * reader, so nobody re-derives the three producers and drifts.
319
+ */
320
+ const contracts = resolveContracts(handler, frond.operationsOverrides, collectorEntityNames);
321
+ /**
322
+ * Who listens to what — read HERE because this is where a contract becomes real,
323
+ * and read from the PLAN rather than from the AST: `{ kind: 'fact' }` is a sentence
324
+ * `computeBindingPlan` already wrote, so nothing re-derives what a parameter is.
325
+ *
326
+ * A subscriber is an ordinary op. It keeps its door, its judge and its middlewares —
327
+ * an emission and a direct call are the same call, which is why nothing here has to
328
+ * build a second path.
329
+ */
330
+ noteSubscriptions(contracts, facadeKey);
331
+ /**
332
+ * A fact is judged on arrival, by the entity it IS.
333
+ *
334
+ * The scan never fills `input` from a parameter type, so a subscriber's payload met
335
+ * no judge at all — tolerable while it came from an emitter in this very process,
336
+ * false the moment it comes off a wire, from another repository, from an older
337
+ * emitter, or out of a queue that held it for three days. A fact is an entity: it
338
+ * has a card, `reconstruct` rebuilds it on the far side, so the same judge stands on
339
+ * both ends — which is already what a door promises.
340
+ *
341
+ * A contract that states its own `input` wins: the three producers keep their order.
342
+ */
343
+ for (const [op, contract] of contracts) {
344
+ if (contract.input)
345
+ continue;
346
+ const bound = contract.binding?.find((b) => b.source.kind === 'fact');
347
+ if (!bound || bound.source.kind !== 'fact')
348
+ continue;
349
+ const shape = entityByName.get(bound.source.factName);
350
+ if (shape)
351
+ contracts.set(op, { ...contract, input: shape });
352
+ }
353
+ /**
354
+ * The field set an op's result is projected onto — the view declared for THAT op
355
+ * (`Crud(Post, { list: PostCard })`), else the handler-wide view
356
+ * (`Crud(Post, PostPublic)`), else the entity. Each op is the audience of its own
357
+ * view: a public index emits cards while `bySlug` emits the full row, from one
358
+ * handler reading one full-row ORM. Resolved once per op, on first call.
359
+ */
360
+ const cachedOutput = new Map();
361
+ const outputFieldsFor = (op) => {
362
+ const known = cachedOutput.get(op);
363
+ if (known)
364
+ return known;
365
+ const perOp = handler?.ctor?.__opOutputs?.[op];
366
+ const contractOutput = contracts.get(op)?.output;
367
+ const schema = (perOp
368
+ ?? contractOutput
369
+ ?? handler?.outputOverride
370
+ ?? handler?.ctor?.__output
371
+ // No entity and nothing declared: there is no shape to project onto, so the
372
+ // result travels as the handler returned it (`encodeFields({}, r)` is `{…r}`).
373
+ ?? entity?.entityClass);
374
+ const fields = typeof schema?.getFields === 'function' ? schema.getFields() : {};
375
+ // A view named for THIS op is a closed list: the author said what this
376
+ // audience gets. The handler-wide forms already narrow at the ORM.
377
+ const resolved = { fields, closed: perOp !== undefined };
378
+ cachedOutput.set(op, resolved);
379
+ return resolved;
380
+ };
381
+ const collectorResolver = (entityName) => {
382
+ const key = collectorKeyOf(entityName);
383
+ try {
384
+ return targetScope.resolve(key);
385
+ }
386
+ catch {
387
+ return undefined;
388
+ }
389
+ };
390
+ // The name the door answers to. It reaches the presenter map below, which is keyed
391
+ // by ENTITY name — they coincide whenever both exist, and when no entity carries
392
+ // this name the lookup simply misses, which is the correct answer.
393
+ const address = handler.address;
394
+ const wrapOp = (op) => (invocation) => {
395
+ const inv = invocation ?? EMPTY_INVOCATION;
396
+ const ctx = { entity: address, operation: op, args: [], state: inv.state, invocation: inv };
397
+ return runMiddlewares(getMiddlewares(address), ctx, async () => {
398
+ const contract = contracts.get(op);
399
+ const schema = contract?.input;
400
+ let effectiveInvocation = inv;
401
+ if (schema && inv.body && typeof inv.body === 'object') {
402
+ // The view's mode travels with it: a partial() input validates as a
403
+ // patch (absent field → untouched), never by forging the fields.
404
+ // No field filter: the axes already judge every case — a client id
405
+ // at create is accepted ({ generate }), an id re-supplied in a
406
+ // patch is 'Immutable', a read-only field 'Read-only', every
407
+ // system-stamped absence is legal via its lifecycle rule, and a
408
+ // key outside the contract is 'Unknown field' (refused, not stripped).
409
+ const result = validateFields(schema.getFields(), inv.body, '', { patch: schema.getOpts?.().patch });
410
+ if (!result.success) {
411
+ throw new FougereError({
412
+ code: ErrorCode.VALIDATION_FAILED,
413
+ message: result.errors.map((e) => `${e.path}: ${e.message}`).join(', '),
414
+ details: result.errors,
415
+ entity: address,
416
+ operation: op,
417
+ });
418
+ }
419
+ effectiveInvocation = { ...inv, body: result.data };
420
+ ctx.invocation = effectiveInvocation;
421
+ }
422
+ // No plan means no declared argument — an op receives what its
423
+ // contract says it receives, never a guess based on its name.
424
+ const resolved = contract?.binding
425
+ ? await resolveArgs(contract.binding, effectiveInvocation, collectorResolver)
426
+ : [];
427
+ // Egress at the boundary: a write-only field never rides the result
428
+ // out, exactly as REST and Pothos already guarantee on their own —
429
+ // then a presenter's computed fields are added, so every door answers
430
+ // the same thing (they used to be applied by the projections alone).
431
+ const out = outputFieldsFor(op);
432
+ const projected = projectEgress(out.fields, await getInstance()[op](...resolved), out.closed);
433
+ if (out.closed)
434
+ return projected;
435
+ const meta = presenterMap.get(address);
436
+ if (!meta)
437
+ return projected;
438
+ // A computed field is bound like an op: what it declares after the rows is
439
+ // resolved from the same invocation, by the same collectors. The plan is
440
+ // computed here and not at scan time because the scan meets presenters
441
+ // before it meets collectors.
442
+ const args = {};
443
+ for (const field of meta.fieldMeta) {
444
+ if (!field.params?.length)
445
+ continue;
446
+ args[field.name] = await resolveArgs(computeBindingPlan(field.params, collectorEntityNames), effectiveInvocation, collectorResolver);
447
+ }
448
+ return presentEgress(projected, scope.resolve(presenterKeyOf(address)), meta.fields, address, op, args);
449
+ });
450
+ };
451
+ // The surface IS the contract table. A method nobody declared is not an
452
+ // op: it stays a method, callable from inside, unreachable from the wire.
453
+ const facade = {};
454
+ for (const op of contracts.keys()) {
455
+ facade[op] = wrapOp(op);
456
+ }
457
+ container.registerValue(facadeKey, facade);
458
+ // The terms alongside the door, under the same audience — a surface that
459
+ // serves fewer ops describes fewer ops.
460
+ container.registerValue(`${facadeKey}:contracts`, contracts);
461
+ return facade;
462
+ };
463
+ // A presenter is about an entity — computed fields sit on a shape — so this walks
464
+ // entities. Exposing the instance lazily; the bridge resolves it on first access.
465
+ for (const entity of frond.entities) {
466
+ if (!presenterMap.has(entity.name))
467
+ continue;
468
+ const presenterKey = presenterKeyOf(entity.name);
469
+ let presenterInstance;
470
+ container.registerValue(presenterKey, new Proxy({}, {
471
+ get(_target, prop) {
472
+ if (!presenterInstance)
473
+ presenterInstance = scope.resolve(presenterKey);
474
+ return presenterInstance[prop];
475
+ },
476
+ }));
477
+ }
478
+ // A facade is about a handler, so this walks HANDLERS. It walked entities before,
479
+ // which made an entity a precondition for being callable at all: a handler naming
480
+ // none was scanned, then never built, and nothing said so.
481
+ for (const handler of defaultHandlers) {
482
+ // Two ways to know the subject, and the explicit one wins: `Crud(Item)` names the
483
+ // entity it was built on, whatever the handler is called. Otherwise the handler's
484
+ // own name is the only thing pointing at one — and pointing at nothing is legal.
485
+ //
486
+ // By NAME, not by identity: the scanner loads an entity through its own loader and
487
+ // the handler imports it through the runtime's, so the same class arrives as two
488
+ // objects. `===` compares module instances, which is not the question being asked.
489
+ const crudTarget = handler.ctor.__entity;
490
+ const subject = crudTarget?.name
491
+ ? crudTarget.name[0].toLowerCase() + crudTarget.name.slice(1)
492
+ : handler.address;
493
+ const entity = frond.entities.find((e) => e.name === subject);
494
+ const facadeKey = facadeKeyOf(handler.address);
495
+ buildFacade(entity, handler, scope, facadeKey);
496
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey)).join(', ')}]`
497
+ + (entity ? '' : ' — no entity of that name: no ORM, no projection, no presenter'));
498
+ }
499
+ // The dual, and it stays: a shape that declares no operation answers nothing. Said
500
+ // once per entity rather than deduced from a silence.
501
+ for (const entity of frond.entities) {
502
+ if (!defaultHandlerMap.has(entity.name)) {
503
+ frondLog.debug(`${entity.name} — entity only, no handler: exposes nothing`);
504
+ }
505
+ }
506
+ // Surface handlers — create sub-scope per surface handler with scoped ORM
507
+ //
508
+ // Pointing at nothing is legal HERE TOO. This loop used to `continue` when no entity
509
+ // carried the handler's name, so `handlers/public/SearchHandler.ts` with no `Search`
510
+ // entity got no door at all and no line saying why — while the very same handler at
511
+ // the default surface is built and logged. One rule, both surfaces.
512
+ for (const handler of surfaceHandlers) {
513
+ const entity = frond.entities.find((e) => e.name === handler.address);
514
+ const surfaceScope = scope.createScope();
515
+ // Register scoped ORM if output override differs from entity
516
+ if (entity && options.ormFactory) {
517
+ const ormName = ormKeyOf(entity.name);
518
+ const baseOrm = options.ormFactory(entity.entityClass, entity.name);
519
+ const outputSchema = handler.outputOverride ?? handler.ctor.__output;
520
+ const scoped = outputSchema && outputSchema !== entity.entityClass
521
+ ? baseOrm.output(outputSchema)
522
+ : baseOrm;
523
+ surfaceScope.registerValue(ormName, guardStorage(scoped, entity.entityClass.getFields(), entity.name));
524
+ }
525
+ const facadeKey = facadeKeyOf(handler.address, handler.surface);
526
+ buildFacade(entity, handler, surfaceScope, facadeKey);
527
+ frondLog.debug(`${facadeKey} [${Object.keys(container.resolve(facadeKey)).join(', ')}]`
528
+ + (entity ? '' : ' — no entity of that name: no ORM, no projection, no presenter'));
529
+ }
530
+ // A named surface is closed, so what it contains is a fact worth stating.
531
+ // Saying it at boot is the difference between a rule and a rule you can
532
+ // check: an entity you meant to serve and never wrote a handler for is
533
+ // absent HERE, in one line, instead of being discovered missing later.
534
+ const surfaceNames = [...new Set(surfaceHandlers.map((h) => h.surface))].sort();
535
+ for (const surfaceName of surfaceNames) {
536
+ const served = surfaceHandlers
537
+ .filter((h) => h.surface === surfaceName)
538
+ .map((h) => h.address)
539
+ .sort();
540
+ const absent = frond.entities.map((e) => e.name).filter((n) => !served.includes(n));
541
+ frondLog.info(`surface '${surfaceName}' — ${served.length} entit${served.length === 1 ? 'y' : 'ies'}: ${served.join(', ')}` +
542
+ (absent.length > 0 ? ` (not served: ${absent.join(', ')})` : ''));
543
+ }
544
+ container.registerValue(`frond:${frond.name}`, scope);
545
+ frondLog.info(`registered — ${frond.entities.length} entities, ${frond.handlers.length} handlers, ${frond.seeds.length} seeds`);
546
+ }
547
+ /**
548
+ * A subscriber refusing the SHAPE, said in one line instead of dumped as an error.
549
+ *
550
+ * This is the one refusal nobody else will ever see. A door hands its 400 back to the
551
+ * caller who can fix it; a fact is dispatched, not delivered, so the sender learns
552
+ * nothing and the log is the whole of the evidence. The most likely cause is also the
553
+ * one a stack trace hides worst — this process's copy is older than the sender's — so
554
+ * the line names the fields and the remedy, and hedges because a genuinely bad payload
555
+ * produces the same refusal.
556
+ */
557
+ const describeRefusal = (fact, cause) => {
558
+ const err = cause;
559
+ if (err?.code !== ErrorCode.VALIDATION_FAILED || !err.details?.length)
560
+ return undefined;
561
+ return `refused the shape — ${err.details.map((d) => `${d.path}: ${d.message}`).join(', ')}.`
562
+ + ` If '${fact}' gained a field, this copy is older than the sender's: re-run \`fougere sync\`.`;
563
+ };
564
+ /**
565
+ * Hand the fact to every listener in THIS process, and give back one promise each.
566
+ *
567
+ * The call goes THROUGH the door, so a subscriber meets the same judge, the same binding
568
+ * and the same middlewares as any caller. Nothing new answers for correctness — that is
569
+ * the dividend of a subscriber being an ordinary op rather than a special kind.
570
+ *
571
+ * It returns the promises rather than settling them, because the two callers want
572
+ * opposite things and only one of them is wrong to wait. See `deliver`.
573
+ */
574
+ const handToListeners = (fact, payload) => {
575
+ const walked = chain.getStore() ?? [];
576
+ if (walked.includes(fact)) {
577
+ throw new Error(`Emission cycle: ${[...walked, fact].join(' → ')}.\n`
578
+ + ` A fact cannot cause itself. One of the subscribers above announces a fact that leads back here.`);
579
+ }
580
+ const listeners = subscribers.get(fact) ?? [];
581
+ if (listeners.length === 0) {
582
+ log.debug(`${fact} — nobody listens in this process`);
583
+ return [];
584
+ }
585
+ const deeper = [...walked, fact];
586
+ return listeners.map(({ door, op }) => ({
587
+ door,
588
+ op,
589
+ done: chain.run(deeper, async () => {
590
+ let facade;
591
+ try {
592
+ facade = container.resolve(door);
593
+ }
594
+ catch (cause) {
595
+ throw new Error(`${fact} → ${door} could not be reached`, { cause });
596
+ }
597
+ return facade[op]({ ...EMPTY_INVOCATION, body: payload });
598
+ }),
599
+ }));
600
+ };
601
+ /**
602
+ * Announcing. Dispatch, never delivery — the emitter is handed back the moment every
603
+ * subscriber has been HANDED the fact, not when any of them is done.
604
+ *
605
+ * The `EventBus` this replaces did `await Promise.all(handlers)` and passed their
606
+ * rejections up, which made a publication hostage to its own indexer.
607
+ */
608
+ const dispatchLocally = async (fact, payload) => {
609
+ for (const { door, op, done } of handToListeners(fact, payload)) {
610
+ void done.catch((cause) => log.error(`${fact} → ${door}.${op}`, describeRefusal(fact, cause) ?? cause));
611
+ }
612
+ };
613
+ /**
614
+ * Receiving. **The opposite rule, deliberately**: this one waits, and it tells.
615
+ *
616
+ * `deliver` is what a CARRIER calls, and a carrier's whole job is to know whether the
617
+ * fact landed — at-least-once is retrying what failed, so a delivery that cannot report
618
+ * makes durability impossible to build on top. It used to be `dispatchLocally` itself:
619
+ * it resolved before any subscriber had run and swallowed every failure into a log, so a
620
+ * queue calling it could only ever ack blindly.
621
+ *
622
+ * That is not a contradiction of "dispatch is not delivery". That rule protects the
623
+ * EMITTER, which must not become hostage to a subscriber; a carrier is not the emitter,
624
+ * it is precisely the party whose business this is.
625
+ *
626
+ * What it still does not do is HOLD anything. A fact refused here is refused, and the
627
+ * carrier decides whether it comes back — which is the whole of Fougere's position on
628
+ * durability: the channel goes underneath, it is not reimplemented here.
629
+ */
630
+ const deliver = async (fact, payload) => {
631
+ const handed = handToListeners(fact, payload);
632
+ const settled = await Promise.allSettled(handed.map((h) => h.done));
633
+ const refused = settled.flatMap((result, i) => result.status === 'rejected' ? [{ ...handed[i], reason: result.reason }] : []);
634
+ for (const { door, op, reason } of refused) {
635
+ log.error(`${fact} → ${door}.${op}`, describeRefusal(fact, reason) ?? reason);
636
+ }
637
+ if (refused.length > 0) {
638
+ throw new AggregateError(refused.map((r) => r.reason), `${fact} — ${refused.length} of ${handed.length} listener(s) refused it`
639
+ + ` (${refused.map((r) => `${r.door}.${r.op}`).join(', ')}).`
640
+ + ` Nothing here holds it: the carrier decides whether it comes back.`);
641
+ }
642
+ };
643
+ // Emitted here, or merely listened to: a process that only subscribes still needs the
644
+ // value, because `deliver` is what a carrier calls and it goes through the same door.
645
+ for (const fact of new Set([...emitted, ...subscribers.keys()])) {
646
+ const shape = entityByName.get(fact);
647
+ container.registerValue(emitKeyOf(fact), async (raw) => {
648
+ /**
649
+ * The announcement realizes the fact's own `lifecycle.create` — an `auto()` stamped,
650
+ * an id generated, a default applied.
651
+ *
652
+ * `validation.ts` states the split: the judge never fills a hole, the STORAGE does,
653
+ * at the point of persistence. A fact has no storage, so nobody did — the judge
654
+ * declared an absent `auto()` legal and omitted it, and a subscriber received a
655
+ * value missing a field its own type promises. Announcing is a fact's point of
656
+ * persistence, and `applyCreate` is the one realization every storage already shares.
657
+ *
658
+ * Here and not in `dispatchLocally`, which is shared with `deliver`: a fact that
659
+ * arrives from elsewhere was stamped by its sender, and stamping it again would give
660
+ * one fact a different identity in every process that relayed it.
661
+ *
662
+ * **A typed emitter cannot reach this yet.** `Emit<T>` names the ROW type, where an
663
+ * `auto()` field is present and required, so `announce({ id, title })` is a
664
+ * compile error and the author writes `at: new Date()` anyway. `CtorInput`
665
+ * (`schema/src/entity.ts`) is exactly the shape wanted and derives from the FIELDS,
666
+ * which the instance type has already thrown away. So this runs for a payload built
667
+ * outside the type — a bridge, a replay, a test — and is inert for everyone else.
668
+ */
669
+ const payload = shape && raw !== null && typeof raw === 'object' && !Array.isArray(raw)
670
+ ? applyCreate(shape.getFields(), raw)
671
+ : raw;
672
+ /**
673
+ * Whoever is not in this process — and it is the ONLY way to reach them.
674
+ *
675
+ * The local dispatch finds its listeners by having READ their code, so it stops at
676
+ * the repository boundary: another team's Frond is not on this disk, and the
677
+ * emission reaches nobody. A carrier hands the fact to a name instead, and the far
678
+ * side subscribes to that same name from ITS own code. Neither reads the other.
679
+ *
680
+ * `deliver` deliberately does NOT come here: a hub that resolved this value to hand
681
+ * on an incoming reading echoed it straight back to the whole fleet.
682
+ */
683
+ const carried = options.onEmit?.(fact, payload);
684
+ if (carried)
685
+ void Promise.resolve(carried).catch((cause) => log.error(`${fact} — carrier refused it`, cause));
686
+ await dispatchLocally(fact, payload);
687
+ });
688
+ }
689
+ /**
690
+ * The last resort, held by the container so every resolution path shares it.
691
+ *
692
+ * It used to live on `app.resolve` alone, which meant the runner found a remote façade
693
+ * and dependency injection did not — a handler asking for a neighbour that had moved
694
+ * got `is not registered` while the very same call over the wire worked. One door now.
695
+ *
696
+ * Surface-scoped keys ('admin:productHandler') stay local: a named surface resolves in
697
+ * this container only, so fabricating one for a remote frond would answer NOT_FOUND on
698
+ * everything (see Known issues).
699
+ */
700
+ container.setFallback?.((name) => {
701
+ if (!remoteRouter)
702
+ return undefined;
703
+ if (!name.endsWith('Handler') || name.includes(':'))
704
+ return undefined;
705
+ // Façade-shaped stand-in; routing happens lazily at the first call.
706
+ return createRemoteFacade(name.replace(/Handler$/, ''), remoteRouter);
707
+ });
708
+ const resolve = (name) => {
709
+ try {
710
+ return container.resolve(name);
711
+ }
712
+ catch (err) {
713
+ if (name.endsWith('Handler') && !name.includes(':') && !remoteRouter) {
714
+ throw new Error(notLoaded(name.replace(/Handler$/, '')));
715
+ }
716
+ throw err;
717
+ }
718
+ };
719
+ const schemaFor = async (entity) => {
720
+ for (const frond of fronds) {
721
+ const found = frond.entities.find((e) => e.name === entity);
722
+ if (found)
723
+ return found.entityClass;
724
+ }
725
+ if (remoteRouter) {
726
+ const route = await remoteRouter.route(entity);
727
+ // A remote door that stores nothing publishes ops and no shape. Saying so beats
728
+ // handing back an empty schema, which would validate every input it was given.
729
+ if (!route.schema) {
730
+ throw new Error(`'${entity}' is served by frond '${route.frond}' but stores no rows, so it has no schema. `
731
+ + `Call its operations through the façade instead.`);
732
+ }
733
+ return route.schema;
734
+ }
735
+ throw new Error(notLoaded(entity));
736
+ };
737
+ const facadeAt = (key, topology) => {
738
+ try {
739
+ return topology
740
+ ? resolve(key)
741
+ : container.resolve(key);
742
+ }
743
+ catch {
744
+ return undefined;
745
+ }
746
+ };
747
+ /**
748
+ * THE membership rule, stated once — every projection reads this and nothing
749
+ * else. It used to live twice, verbatim, in the REST and GraphQL adapters,
750
+ * and two other readers invented their own answer.
751
+ *
752
+ * Naming an audience closes it. Three ways to name an entity into a surface,
753
+ * in precedence order:
754
+ * - `surfaces:` in frond.config.ts — when the list exists, it IS the list;
755
+ * - a handler under `handlers/<surface>/` — which also restricts the façade;
756
+ * - the `@expose` sugar, resolved into the same two by the scan.
757
+ * What none of them names is not served. It used to be the reverse: no
758
+ * `public:categoryHandler` meant the FULL CategoryHandler rode the public
759
+ * door, create/update/delete included (measured on demos/nuxt-blog).
760
+ *
761
+ * A named entity with no façade of its own falls back to the default one —
762
+ * that is not the old widening, it is the author saying "this one, as it is".
763
+ * The leak was exposure with no statement behind it.
764
+ *
765
+ * A surface key stays local: a doublure serves whatever the remote's own door
766
+ * serves, which is the remote's business and not ours to re-audience.
767
+ */
768
+ const facadeFor = (entity, surface) => {
769
+ if (!surface)
770
+ return facadeAt(facadeKeyOf(entity), true);
771
+ const own = facadeAt(facadeKeyOf(entity, surface), false);
772
+ const declared = fronds.find((f) => f.entities.some((e) => e.name === entity))?.surfaces?.[surface];
773
+ if (!declared)
774
+ return own;
775
+ return declared.some((n) => n.toLowerCase() === entity.toLowerCase())
776
+ ? (own ?? facadeAt(facadeKeyOf(entity), false))
777
+ : undefined;
778
+ };
779
+ /**
780
+ * The storage an entity is backed by — the dual of `facadeFor`, which serves its
781
+ * client-facing door. Both are ways in; an entity that opens none of them still has rows.
782
+ *
783
+ * It resolves through the owning frond's scope, because that is where entity ORMs live:
784
+ * `resolve('UserOrm')` reads the ROOT container and never finds one, so callers outside
785
+ * the frond concluded there was no storage. The seed loop did exactly that, and skipped
786
+ * every entity with no façade — the one case its own fallback existed for.
787
+ */
788
+ const ormFor = (entity) => {
789
+ const owner = fronds.find((f) => f.entities.some((e) => e.name === entity));
790
+ if (!owner)
791
+ return undefined;
792
+ const key = ormKeyOf(entity);
793
+ try {
794
+ return container.resolve(`frond:${owner.name}`).resolve(key);
795
+ }
796
+ catch {
797
+ return undefined;
798
+ }
799
+ };
800
+ return {
801
+ container,
802
+ fronds,
803
+ resolve,
804
+ schemaFor,
805
+ facadeFor,
806
+ listensTo: () => [...subscribers.keys()],
807
+ deliver,
808
+ ormFor,
809
+ dispose: () => container.dispose(),
810
+ [Symbol.asyncDispose]: () => container.dispose(),
811
+ use(...args) {
812
+ if (typeof args[0] === 'string') {
813
+ const [entity, mw] = args;
814
+ const list = scopedMiddlewares.get(entity) ?? [];
815
+ list.push(mw);
816
+ scopedMiddlewares.set(entity, list);
817
+ }
818
+ else {
819
+ globalMiddlewares.push(args[0]);
820
+ }
821
+ },
822
+ auth: authRuntime,
823
+ };
824
+ }
825
+ //# sourceMappingURL=bootstrap.js.map