@beignet/core 0.0.13 → 0.0.14

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +156 -2
  3. package/dist/entitlements/index.d.ts +234 -0
  4. package/dist/entitlements/index.d.ts.map +1 -0
  5. package/dist/entitlements/index.js +172 -0
  6. package/dist/entitlements/index.js.map +1 -0
  7. package/dist/error-reporting/index.d.ts +131 -0
  8. package/dist/error-reporting/index.d.ts.map +1 -0
  9. package/dist/error-reporting/index.js +112 -0
  10. package/dist/error-reporting/index.js.map +1 -0
  11. package/dist/flags/index.d.ts +293 -0
  12. package/dist/flags/index.d.ts.map +1 -0
  13. package/dist/flags/index.js +204 -0
  14. package/dist/flags/index.js.map +1 -0
  15. package/dist/locks/index.d.ts +155 -0
  16. package/dist/locks/index.d.ts.map +1 -0
  17. package/dist/locks/index.js +248 -0
  18. package/dist/locks/index.js.map +1 -0
  19. package/dist/payments/index.d.ts +430 -0
  20. package/dist/payments/index.d.ts.map +1 -0
  21. package/dist/payments/index.js +230 -0
  22. package/dist/payments/index.js.map +1 -0
  23. package/dist/ports/index.d.ts +61 -1
  24. package/dist/ports/index.d.ts.map +1 -1
  25. package/dist/ports/index.js +33 -0
  26. package/dist/ports/index.js.map +1 -1
  27. package/dist/ports/policy.d.ts +104 -0
  28. package/dist/ports/policy.d.ts.map +1 -1
  29. package/dist/ports/policy.js +102 -7
  30. package/dist/ports/policy.js.map +1 -1
  31. package/dist/search/index.d.ts +175 -0
  32. package/dist/search/index.d.ts.map +1 -0
  33. package/dist/search/index.js +344 -0
  34. package/dist/search/index.js.map +1 -0
  35. package/dist/server/server.d.ts.map +1 -1
  36. package/dist/server/server.js +9 -1
  37. package/dist/server/server.js.map +1 -1
  38. package/dist/testing/index.d.ts +46 -1
  39. package/dist/testing/index.d.ts.map +1 -1
  40. package/dist/testing/index.js +33 -1
  41. package/dist/testing/index.js.map +1 -1
  42. package/package.json +25 -1
  43. package/src/entitlements/index.ts +479 -0
  44. package/src/error-reporting/index.ts +273 -0
  45. package/src/flags/index.ts +608 -0
  46. package/src/locks/index.ts +440 -0
  47. package/src/payments/index.ts +699 -0
  48. package/src/ports/index.ts +194 -0
  49. package/src/ports/policy.ts +272 -7
  50. package/src/search/index.ts +632 -0
  51. package/src/server/server.ts +15 -0
  52. package/src/testing/index.ts +83 -1
@@ -0,0 +1,632 @@
1
+ /**
2
+ * @beignet/core/search
3
+ *
4
+ * Provider-neutral search primitives for Beignet applications.
5
+ */
6
+
7
+ import {
8
+ createProvider,
9
+ createProviderInstrumentation,
10
+ } from "../providers/index.js";
11
+
12
+ /**
13
+ * Primitive values accepted in indexed documents and provider-neutral filters.
14
+ */
15
+ export type SearchPrimitive = null | boolean | number | string;
16
+
17
+ /**
18
+ * JSON-like values accepted in indexed documents.
19
+ */
20
+ export type SearchValue =
21
+ | SearchPrimitive
22
+ | readonly SearchValue[]
23
+ | { readonly [key: string]: SearchValue };
24
+
25
+ /**
26
+ * Minimal document shape accepted by search indexes.
27
+ */
28
+ export type SearchDocumentBase = {
29
+ id: string;
30
+ };
31
+
32
+ /**
33
+ * General search document shape.
34
+ */
35
+ export type SearchDocument = SearchDocumentBase & {
36
+ readonly [field: string]: SearchValue | undefined;
37
+ };
38
+
39
+ type SearchField<TDocument extends SearchDocumentBase> = Extract<
40
+ keyof TDocument,
41
+ string
42
+ >;
43
+
44
+ /**
45
+ * Provider-neutral index definition.
46
+ */
47
+ export type SearchIndexDef<
48
+ TDocument extends SearchDocumentBase = SearchDocument,
49
+ > = {
50
+ name: string;
51
+ primaryKey: SearchField<TDocument>;
52
+ searchableAttributes?: readonly SearchField<TDocument>[];
53
+ filterableAttributes?: readonly SearchField<TDocument>[];
54
+ sortableAttributes?: readonly SearchField<TDocument>[];
55
+ displayedAttributes?: readonly SearchField<TDocument>[];
56
+ metadata?: Record<string, unknown>;
57
+ };
58
+
59
+ /**
60
+ * Options accepted when defining a search index.
61
+ */
62
+ export type DefineSearchIndexOptions<TDocument extends SearchDocumentBase> =
63
+ Omit<SearchIndexDef<TDocument>, "name" | "primaryKey"> & {
64
+ primaryKey?: SearchField<TDocument>;
65
+ };
66
+
67
+ /**
68
+ * Provider-neutral filter values.
69
+ */
70
+ export type SearchFilterValue =
71
+ | SearchPrimitive
72
+ | readonly Exclude<SearchPrimitive, null>[];
73
+
74
+ /**
75
+ * Provider-neutral exact-match filters.
76
+ */
77
+ export type SearchFilters = Record<string, SearchFilterValue>;
78
+
79
+ /**
80
+ * Provider-neutral sort expression. Use `field:asc` or `field:desc`.
81
+ */
82
+ export type SearchSort = `${string}:asc` | `${string}:desc` | (string & {});
83
+
84
+ /**
85
+ * Query accepted by search providers.
86
+ */
87
+ export type SearchQuery = {
88
+ query?: string;
89
+ filters?: SearchFilters;
90
+ sort?: readonly SearchSort[];
91
+ facets?: readonly string[];
92
+ limit?: number;
93
+ offset?: number;
94
+ };
95
+
96
+ /**
97
+ * Page metadata returned from search providers.
98
+ */
99
+ export type SearchResultPage = {
100
+ kind: "offset";
101
+ limit: number;
102
+ offset: number;
103
+ total?: number;
104
+ hasMore: boolean;
105
+ };
106
+
107
+ /**
108
+ * Search result payload.
109
+ */
110
+ export type SearchResults<TDocument extends SearchDocumentBase> = {
111
+ hits: TDocument[];
112
+ query: string;
113
+ page: SearchResultPage;
114
+ processingTimeMs?: number;
115
+ facets?: Record<string, Record<string, number>>;
116
+ };
117
+
118
+ /**
119
+ * Result returned after indexing documents.
120
+ */
121
+ export type SearchIndexResult = {
122
+ indexed: number;
123
+ taskId?: string | number;
124
+ };
125
+
126
+ /**
127
+ * Result returned after deleting indexed documents.
128
+ */
129
+ export type SearchDeleteResult = {
130
+ deleted: number;
131
+ taskId?: string | number;
132
+ };
133
+
134
+ /**
135
+ * App-facing search port.
136
+ */
137
+ export type SearchPort = {
138
+ configureIndex<TDocument extends SearchDocumentBase>(
139
+ index: SearchIndexDef<TDocument>,
140
+ ): Promise<void>;
141
+ indexDocuments<TDocument extends SearchDocumentBase>(
142
+ index: SearchIndexDef<TDocument>,
143
+ documents: TDocument | readonly TDocument[],
144
+ ): Promise<SearchIndexResult>;
145
+ deleteDocuments<TDocument extends SearchDocumentBase>(
146
+ index: SearchIndexDef<TDocument>,
147
+ ids: string | readonly string[],
148
+ ): Promise<SearchDeleteResult>;
149
+ clearIndex<TDocument extends SearchDocumentBase>(
150
+ index: SearchIndexDef<TDocument>,
151
+ ): Promise<SearchDeleteResult>;
152
+ search<TDocument extends SearchDocumentBase>(
153
+ index: SearchIndexDef<TDocument>,
154
+ query: SearchQuery,
155
+ ): Promise<SearchResults<TDocument>>;
156
+ };
157
+
158
+ /**
159
+ * Captured memory index state exposed for tests.
160
+ */
161
+ export type MemorySearchIndexState<
162
+ TDocument extends SearchDocumentBase = SearchDocument,
163
+ > = {
164
+ definition?: SearchIndexDef<TDocument>;
165
+ documents: Map<string, TDocument>;
166
+ };
167
+
168
+ /**
169
+ * In-memory search port exposed for assertions in tests.
170
+ */
171
+ export type MemorySearchPort = SearchPort & {
172
+ indexes: Map<string, MemorySearchIndexState>;
173
+ reset(index?: SearchIndexDef): void;
174
+ };
175
+
176
+ /**
177
+ * Options for the in-memory search adapter.
178
+ */
179
+ export type CreateMemorySearchOptions = {
180
+ now?: () => Date;
181
+ };
182
+
183
+ /**
184
+ * Options for the in-memory search provider.
185
+ */
186
+ export type MemorySearchProviderOptions = CreateMemorySearchOptions & {
187
+ name?: string;
188
+ };
189
+
190
+ /**
191
+ * Ports contributed by the memory search provider.
192
+ */
193
+ export interface MemorySearchProviderPorts {
194
+ search: SearchPort;
195
+ }
196
+
197
+ /**
198
+ * Error thrown when search inputs are invalid.
199
+ */
200
+ export class SearchOptionsError extends Error {
201
+ constructor(message: string) {
202
+ super(message);
203
+ this.name = "SearchOptionsError";
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Define a typed search index.
209
+ */
210
+ export function defineSearchIndex<
211
+ TDocument extends SearchDocumentBase = SearchDocument,
212
+ >(
213
+ name: string,
214
+ options: DefineSearchIndexOptions<TDocument> = {},
215
+ ): SearchIndexDef<TDocument> {
216
+ if (!name) throw new SearchOptionsError("Search index name is required.");
217
+
218
+ return {
219
+ name,
220
+ primaryKey: options.primaryKey ?? ("id" as SearchField<TDocument>),
221
+ searchableAttributes: options.searchableAttributes,
222
+ filterableAttributes: options.filterableAttributes,
223
+ sortableAttributes: options.sortableAttributes,
224
+ displayedAttributes: options.displayedAttributes,
225
+ metadata: options.metadata,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Index documents through any `SearchPort`.
231
+ */
232
+ export function indexSearchDocuments<TDocument extends SearchDocumentBase>(
233
+ search: SearchPort,
234
+ index: SearchIndexDef<TDocument>,
235
+ documents: TDocument | readonly TDocument[],
236
+ ): Promise<SearchIndexResult> {
237
+ return search.indexDocuments(index, documents);
238
+ }
239
+
240
+ /**
241
+ * Query documents through any `SearchPort`.
242
+ */
243
+ export function searchDocuments<TDocument extends SearchDocumentBase>(
244
+ search: SearchPort,
245
+ index: SearchIndexDef<TDocument>,
246
+ query: SearchQuery,
247
+ ): Promise<SearchResults<TDocument>> {
248
+ return search.search(index, query);
249
+ }
250
+
251
+ /**
252
+ * Create an in-memory search port for tests and single-process development.
253
+ */
254
+ export function createMemorySearch(
255
+ _options: CreateMemorySearchOptions = {},
256
+ ): MemorySearchPort {
257
+ const indexes = new Map<string, MemorySearchIndexState>();
258
+
259
+ const port: MemorySearchPort = {
260
+ indexes,
261
+ async configureIndex(index) {
262
+ validateIndex(index);
263
+ stateFor(indexes, index).definition = index;
264
+ },
265
+ async indexDocuments(index, documents) {
266
+ validateIndex(index);
267
+ const state = stateFor(indexes, index);
268
+ state.definition = state.definition ?? index;
269
+ const list = Array.isArray(documents) ? documents : [documents];
270
+
271
+ for (const document of list) {
272
+ validateDocument(index, document);
273
+ state.documents.set(
274
+ String(document[index.primaryKey]),
275
+ clone(document),
276
+ );
277
+ }
278
+
279
+ return { indexed: list.length };
280
+ },
281
+ async deleteDocuments(index, ids) {
282
+ validateIndex(index);
283
+ const state = stateFor(indexes, index);
284
+ const list = Array.isArray(ids) ? ids : [ids];
285
+ let deleted = 0;
286
+
287
+ for (const id of list) {
288
+ if (state.documents.delete(id)) deleted++;
289
+ }
290
+
291
+ return { deleted };
292
+ },
293
+ async clearIndex(index) {
294
+ validateIndex(index);
295
+ const state = stateFor(indexes, index);
296
+ const deleted = state.documents.size;
297
+ state.documents.clear();
298
+ return { deleted };
299
+ },
300
+ async search(index, query) {
301
+ validateIndex(index);
302
+ validateQuery(query);
303
+ const state = stateFor(indexes, index);
304
+ const effectiveIndex = state.definition ?? index;
305
+ const queryText = query.query?.trim() ?? "";
306
+ const limit = query.limit ?? 20;
307
+ const offset = query.offset ?? 0;
308
+ const filtered = [...state.documents.values()]
309
+ .filter((document) => matchesFilters(document, query.filters))
310
+ .filter((document) =>
311
+ matchesQuery(document, effectiveIndex, queryText),
312
+ );
313
+ const sorted = sortDocuments(filtered, query.sort);
314
+ const hits = sorted.slice(offset, offset + limit).map(clone);
315
+
316
+ return {
317
+ hits,
318
+ query: queryText,
319
+ page: {
320
+ kind: "offset",
321
+ limit,
322
+ offset,
323
+ total: filtered.length,
324
+ hasMore: offset + hits.length < filtered.length,
325
+ },
326
+ facets: buildFacets(sorted, query.facets),
327
+ };
328
+ },
329
+ reset(index) {
330
+ if (index) {
331
+ indexes.delete(index.name);
332
+ return;
333
+ }
334
+
335
+ indexes.clear();
336
+ },
337
+ };
338
+
339
+ return port;
340
+ }
341
+
342
+ /**
343
+ * Create a provider that contributes an in-memory search port.
344
+ */
345
+ export function createMemorySearchProvider(
346
+ options: MemorySearchProviderOptions = {},
347
+ ) {
348
+ const { name = "memory-search", ...searchOptions } = options;
349
+
350
+ return createProvider({
351
+ name,
352
+ metadata: {
353
+ ports: ["search"],
354
+ watchers: ["search"],
355
+ },
356
+ setup({ ports }) {
357
+ const instrumentation = createProviderInstrumentation(ports, {
358
+ providerName: name,
359
+ watcher: "search",
360
+ });
361
+
362
+ return {
363
+ ports: {
364
+ search: instrumentSearch(
365
+ createMemorySearch(searchOptions),
366
+ instrumentation,
367
+ ),
368
+ } satisfies MemorySearchProviderPorts,
369
+ };
370
+ },
371
+ });
372
+ }
373
+
374
+ function instrumentSearch(
375
+ search: SearchPort,
376
+ instrumentation: ReturnType<typeof createProviderInstrumentation>,
377
+ ): SearchPort {
378
+ return {
379
+ async configureIndex(index) {
380
+ const startedAt = Date.now();
381
+ await search.configureIndex(index);
382
+ instrumentation.custom({
383
+ name: "search.configureIndex",
384
+ label: "Search index configured",
385
+ summary: index.name,
386
+ details: { index: index.name, durationMs: Date.now() - startedAt },
387
+ });
388
+ },
389
+ async indexDocuments(index, documents) {
390
+ const startedAt = Date.now();
391
+ const result = await search.indexDocuments(index, documents);
392
+ instrumentation.custom({
393
+ name: "search.indexDocuments",
394
+ label: "Search documents indexed",
395
+ summary: `${index.name}: ${result.indexed}`,
396
+ details: {
397
+ index: index.name,
398
+ indexed: result.indexed,
399
+ durationMs: Date.now() - startedAt,
400
+ },
401
+ });
402
+ return result;
403
+ },
404
+ async deleteDocuments(index, ids) {
405
+ const startedAt = Date.now();
406
+ const result = await search.deleteDocuments(index, ids);
407
+ instrumentation.custom({
408
+ name: "search.deleteDocuments",
409
+ label: "Search documents deleted",
410
+ summary: `${index.name}: ${result.deleted}`,
411
+ details: {
412
+ index: index.name,
413
+ deleted: result.deleted,
414
+ durationMs: Date.now() - startedAt,
415
+ },
416
+ });
417
+ return result;
418
+ },
419
+ async clearIndex(index) {
420
+ const startedAt = Date.now();
421
+ const result = await search.clearIndex(index);
422
+ instrumentation.custom({
423
+ name: "search.clearIndex",
424
+ label: "Search index cleared",
425
+ summary: `${index.name}: ${result.deleted}`,
426
+ details: {
427
+ index: index.name,
428
+ deleted: result.deleted,
429
+ durationMs: Date.now() - startedAt,
430
+ },
431
+ });
432
+ return result;
433
+ },
434
+ async search(index, query) {
435
+ const startedAt = Date.now();
436
+ const result = await search.search(index, query);
437
+ instrumentation.custom({
438
+ name: "search.query",
439
+ label: "Search query",
440
+ summary: `${index.name}: ${result.query}`,
441
+ details: {
442
+ index: index.name,
443
+ query: result.query,
444
+ hits: result.hits.length,
445
+ total: result.page.total,
446
+ durationMs: Date.now() - startedAt,
447
+ },
448
+ });
449
+ return result;
450
+ },
451
+ };
452
+ }
453
+
454
+ function stateFor<TDocument extends SearchDocumentBase>(
455
+ indexes: Map<string, MemorySearchIndexState>,
456
+ index: SearchIndexDef<TDocument>,
457
+ ): MemorySearchIndexState<TDocument> {
458
+ let state = indexes.get(index.name);
459
+ if (!state) {
460
+ state = { documents: new Map() };
461
+ indexes.set(index.name, state);
462
+ }
463
+ return state as unknown as MemorySearchIndexState<TDocument>;
464
+ }
465
+
466
+ function validateIndex<TDocument extends SearchDocumentBase>(
467
+ index: SearchIndexDef<TDocument>,
468
+ ) {
469
+ if (!index.name)
470
+ throw new SearchOptionsError("Search index name is required.");
471
+ if (!index.primaryKey) {
472
+ throw new SearchOptionsError("Search index primaryKey is required.");
473
+ }
474
+ }
475
+
476
+ function validateDocument<TDocument extends SearchDocumentBase>(
477
+ index: SearchIndexDef<TDocument>,
478
+ document: TDocument,
479
+ ) {
480
+ const id = document[index.primaryKey];
481
+ if (typeof id !== "string" || id.length === 0) {
482
+ throw new SearchOptionsError(
483
+ `Search document for index "${index.name}" must include a non-empty "${index.primaryKey}" string.`,
484
+ );
485
+ }
486
+ }
487
+
488
+ function validateQuery(query: SearchQuery) {
489
+ if (
490
+ query.limit !== undefined &&
491
+ (!Number.isFinite(query.limit) ||
492
+ !Number.isInteger(query.limit) ||
493
+ query.limit < 1)
494
+ ) {
495
+ throw new SearchOptionsError(
496
+ "Search query limit must be a positive integer.",
497
+ );
498
+ }
499
+ if (
500
+ query.offset !== undefined &&
501
+ (!Number.isFinite(query.offset) ||
502
+ !Number.isInteger(query.offset) ||
503
+ query.offset < 0)
504
+ ) {
505
+ throw new SearchOptionsError(
506
+ "Search query offset must be a zero or greater integer.",
507
+ );
508
+ }
509
+ }
510
+
511
+ function matchesFilters(
512
+ document: SearchDocumentBase,
513
+ filters: SearchFilters | undefined,
514
+ ): boolean {
515
+ if (!filters) return true;
516
+
517
+ return Object.entries(filters).every(([field, expected]) => {
518
+ const actual = (document as Record<string, unknown>)[field];
519
+ return matchesFilterValue(actual, expected);
520
+ });
521
+ }
522
+
523
+ function matchesFilterValue(
524
+ actual: unknown,
525
+ expected: SearchFilterValue,
526
+ ): boolean {
527
+ if (Array.isArray(expected)) {
528
+ if (Array.isArray(actual)) {
529
+ return actual.some((entry) =>
530
+ expected.some((expectedEntry) => expectedEntry === entry),
531
+ );
532
+ }
533
+
534
+ return expected.some((entry) => entry === actual);
535
+ }
536
+
537
+ if (Array.isArray(actual)) {
538
+ return actual.some((entry) => entry === expected);
539
+ }
540
+
541
+ return actual === expected;
542
+ }
543
+
544
+ function matchesQuery<TDocument extends SearchDocumentBase>(
545
+ document: TDocument,
546
+ index: SearchIndexDef<TDocument>,
547
+ query: string,
548
+ ): boolean {
549
+ if (!query) return true;
550
+
551
+ const needle = query.toLowerCase();
552
+ const fields =
553
+ index.searchableAttributes ??
554
+ (Object.keys(document) as SearchField<TDocument>[]);
555
+
556
+ return fields.some((field) =>
557
+ searchableText((document as Record<string, unknown>)[field])
558
+ .toLowerCase()
559
+ .includes(needle),
560
+ );
561
+ }
562
+
563
+ function sortDocuments<TDocument extends SearchDocumentBase>(
564
+ documents: TDocument[],
565
+ sort: readonly SearchSort[] | undefined,
566
+ ): TDocument[] {
567
+ if (!sort || sort.length === 0) return documents;
568
+
569
+ return [...documents].sort((left, right) => {
570
+ for (const expression of sort) {
571
+ const [field, direction = "asc"] = expression.split(":");
572
+ const result = compareValues(
573
+ (left as Record<string, unknown>)[field],
574
+ (right as Record<string, unknown>)[field],
575
+ );
576
+ if (result !== 0) {
577
+ return direction === "desc" ? -result : result;
578
+ }
579
+ }
580
+
581
+ return 0;
582
+ });
583
+ }
584
+
585
+ function compareValues(left: unknown, right: unknown): number {
586
+ if (left === right) return 0;
587
+ if (left === undefined || left === null) return 1;
588
+ if (right === undefined || right === null) return -1;
589
+ if (typeof left === "number" && typeof right === "number") {
590
+ return left - right;
591
+ }
592
+
593
+ return String(left).localeCompare(String(right));
594
+ }
595
+
596
+ function buildFacets<TDocument extends SearchDocumentBase>(
597
+ documents: readonly TDocument[],
598
+ facets: readonly string[] | undefined,
599
+ ): Record<string, Record<string, number>> | undefined {
600
+ if (!facets || facets.length === 0) return undefined;
601
+
602
+ const result: Record<string, Record<string, number>> = {};
603
+ for (const facet of facets) {
604
+ const counts: Record<string, number> = {};
605
+ for (const document of documents) {
606
+ const value = (document as Record<string, unknown>)[facet];
607
+ const values = Array.isArray(value) ? value : [value];
608
+ for (const entry of values) {
609
+ if (entry === undefined || entry === null) continue;
610
+ const key = String(entry);
611
+ counts[key] = (counts[key] ?? 0) + 1;
612
+ }
613
+ }
614
+ result[facet] = counts;
615
+ }
616
+
617
+ return result;
618
+ }
619
+
620
+ function searchableText(value: unknown): string {
621
+ if (value === undefined || value === null) return "";
622
+ if (Array.isArray(value)) return value.map(searchableText).join(" ");
623
+ if (typeof value === "object") {
624
+ return Object.values(value).map(searchableText).join(" ");
625
+ }
626
+
627
+ return String(value);
628
+ }
629
+
630
+ function clone<T>(value: T): T {
631
+ return JSON.parse(JSON.stringify(value)) as T;
632
+ }
@@ -27,6 +27,7 @@ import {
27
27
  import type { AnyPorts } from "../ports/index.js";
28
28
  import {
29
29
  AuthUnauthorizedError,
30
+ EntitlementRequiredError,
30
31
  GateAuthorizationError,
31
32
  isUnboundPort,
32
33
  TenantRequiredError,
@@ -1158,6 +1159,20 @@ function createRequestExecutor<
1158
1159
  };
1159
1160
  }
1160
1161
 
1162
+ if (currentError instanceof EntitlementRequiredError) {
1163
+ return {
1164
+ ctx,
1165
+ response: errorResponse(
1166
+ currentError.status,
1167
+ currentError.code,
1168
+ currentError.message,
1169
+ currentError.details,
1170
+ ),
1171
+ error: currentError,
1172
+ owner: "framework",
1173
+ };
1174
+ }
1175
+
1161
1176
  for (const hook of hooks) {
1162
1177
  if (!hook.mapUnhandledError) continue;
1163
1178
  try {