@beignet/core 0.0.13 → 0.0.15

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 (70) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +198 -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 +74 -1
  24. package/dist/ports/index.d.ts.map +1 -1
  25. package/dist/ports/index.js +38 -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/hooks/error-reporting.d.ts +79 -0
  36. package/dist/server/hooks/error-reporting.d.ts.map +1 -0
  37. package/dist/server/hooks/error-reporting.js +147 -0
  38. package/dist/server/hooks/error-reporting.js.map +1 -0
  39. package/dist/server/hooks/index.d.ts +1 -0
  40. package/dist/server/hooks/index.d.ts.map +1 -1
  41. package/dist/server/hooks/index.js +1 -0
  42. package/dist/server/hooks/index.js.map +1 -1
  43. package/dist/server/server.d.ts +5 -0
  44. package/dist/server/server.d.ts.map +1 -1
  45. package/dist/server/server.js +9 -1
  46. package/dist/server/server.js.map +1 -1
  47. package/dist/server/use-case-route.d.ts +2 -2
  48. package/dist/testing/index.d.ts +46 -1
  49. package/dist/testing/index.d.ts.map +1 -1
  50. package/dist/testing/index.js +33 -1
  51. package/dist/testing/index.js.map +1 -1
  52. package/dist/webhooks/index.d.ts +195 -0
  53. package/dist/webhooks/index.d.ts.map +1 -0
  54. package/dist/webhooks/index.js +313 -0
  55. package/dist/webhooks/index.js.map +1 -0
  56. package/package.json +29 -1
  57. package/src/entitlements/index.ts +479 -0
  58. package/src/error-reporting/index.ts +273 -0
  59. package/src/flags/index.ts +608 -0
  60. package/src/locks/index.ts +440 -0
  61. package/src/payments/index.ts +699 -0
  62. package/src/ports/index.ts +231 -0
  63. package/src/ports/policy.ts +272 -7
  64. package/src/search/index.ts +632 -0
  65. package/src/server/hooks/error-reporting.ts +249 -0
  66. package/src/server/hooks/index.ts +8 -0
  67. package/src/server/server.ts +20 -0
  68. package/src/server/use-case-route.ts +2 -2
  69. package/src/testing/index.ts +83 -1
  70. package/src/webhooks/index.ts +555 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Error reporting hook utilities for @beignet/core/server
3
+ */
4
+
5
+ import type { HttpContractConfig } from "../../contracts/index.js";
6
+ import type {
7
+ ErrorReporterPort,
8
+ ErrorReportOptions,
9
+ } from "../../error-reporting/index.js";
10
+ import { isAppError, SchemaValidationError } from "../../errors/index.js";
11
+ import {
12
+ IdempotencyConflictError,
13
+ IdempotencyInProgressError,
14
+ } from "../../idempotency/index.js";
15
+ import {
16
+ AuthUnauthorizedError,
17
+ EntitlementRequiredError,
18
+ GateAuthorizationError,
19
+ TenantRequiredError,
20
+ } from "../../ports/index.js";
21
+ import type {
22
+ HttpRequestLike,
23
+ ServerCaughtErrorHook,
24
+ ServerHook,
25
+ } from "../types.js";
26
+
27
+ type MaybePromise<T> = T | Promise<T>;
28
+
29
+ /**
30
+ * Minimal context shape used by `createErrorReportingHooks(...)`.
31
+ */
32
+ export type ErrorReportingContext = {
33
+ ports?: {
34
+ errorReporter?: ErrorReporterPort;
35
+ };
36
+ requestId?: string;
37
+ traceId?: string;
38
+ spanId?: string;
39
+ parentSpanId?: string;
40
+ traceparent?: string;
41
+ actor?: {
42
+ type?: string;
43
+ id?: string;
44
+ };
45
+ tenant?: {
46
+ id?: string;
47
+ };
48
+ };
49
+
50
+ /**
51
+ * Arguments passed to error reporting hook callbacks.
52
+ */
53
+ export type ErrorReportingHookArgs<Ctx> = Parameters<
54
+ ServerCaughtErrorHook<Ctx>
55
+ >[0];
56
+
57
+ /**
58
+ * Error reporter resolver accepted by `createErrorReportingHooks(...)`.
59
+ */
60
+ export type ErrorReporterResolver<Ctx> = (
61
+ args: ErrorReportingHookArgs<Ctx>,
62
+ ) => MaybePromise<ErrorReporterPort | undefined>;
63
+
64
+ /**
65
+ * Options accepted by `createErrorReportingHooks(...)`.
66
+ */
67
+ export interface ErrorReportingHooksOptions<Ctx> {
68
+ /**
69
+ * Reporter to use instead of `ctx.ports.errorReporter`.
70
+ */
71
+ reporter?: ErrorReporterPort | ErrorReporterResolver<Ctx>;
72
+ /**
73
+ * Decide whether a caught error should be captured. The default reports
74
+ * unexpected failures and skips expected catalog/auth/validation outcomes.
75
+ */
76
+ shouldReport?: (args: ErrorReportingHookArgs<Ctx>) => MaybePromise<boolean>;
77
+ /**
78
+ * Additional capture options. These are merged over Beignet's request,
79
+ * actor, tenant, contract, and trace metadata.
80
+ */
81
+ reportOptions?:
82
+ | ErrorReportOptions
83
+ | ((args: ErrorReportingHookArgs<Ctx>) => MaybePromise<ErrorReportOptions>);
84
+ /**
85
+ * Observer for reporter failures. Reporter failures are otherwise ignored so
86
+ * error reporting cannot change the HTTP response.
87
+ */
88
+ onReporterError?: (args: {
89
+ error: unknown;
90
+ reportingError: unknown;
91
+ ctx?: Ctx;
92
+ req: HttpRequestLike;
93
+ contract: HttpContractConfig;
94
+ }) => MaybePromise<void>;
95
+ }
96
+
97
+ /**
98
+ * Create server hooks that capture unexpected HTTP request failures through
99
+ * `ctx.ports.errorReporter`.
100
+ *
101
+ * The hook observes `onCaughtError`, never maps responses, and ignores capture
102
+ * failures by default so reporting cannot affect request handling.
103
+ */
104
+ export function createErrorReportingHooks<
105
+ Ctx extends ErrorReportingContext = ErrorReportingContext,
106
+ >(config: ErrorReportingHooksOptions<Ctx> = {}): ServerHook<Ctx> {
107
+ return {
108
+ name: "error-reporting",
109
+ async onCaughtError(args) {
110
+ const shouldReport = config.shouldReport
111
+ ? await config.shouldReport(args)
112
+ : shouldReportServerError(args.err);
113
+ if (!shouldReport) return;
114
+
115
+ const reporter = await resolveReporter(config.reporter, args);
116
+ if (!reporter) return;
117
+
118
+ const baseOptions = createDefaultReportOptions(args);
119
+ const customOptions =
120
+ typeof config.reportOptions === "function"
121
+ ? await config.reportOptions(args)
122
+ : config.reportOptions;
123
+
124
+ try {
125
+ await reporter.captureException(
126
+ args.err,
127
+ mergeReportOptions(baseOptions, customOptions),
128
+ );
129
+ } catch (reportingError) {
130
+ try {
131
+ await config.onReporterError?.({
132
+ error: args.err,
133
+ reportingError,
134
+ ctx: args.ctx,
135
+ req: args.req,
136
+ contract: args.contract,
137
+ });
138
+ } catch {
139
+ // Reporter failure observers must not affect request handling.
140
+ }
141
+ }
142
+ },
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Default filter for HTTP error reporting.
148
+ */
149
+ export function shouldReportServerError(error: unknown): boolean {
150
+ if (isAppError(error)) return false;
151
+ if (error instanceof AuthUnauthorizedError) return false;
152
+ if (error instanceof TenantRequiredError) return false;
153
+ if (error instanceof IdempotencyConflictError) return false;
154
+ if (error instanceof IdempotencyInProgressError) return false;
155
+ if (error instanceof GateAuthorizationError) return false;
156
+ if (error instanceof EntitlementRequiredError) return false;
157
+ if (error instanceof SchemaValidationError) return false;
158
+ return true;
159
+ }
160
+
161
+ async function resolveReporter<Ctx extends ErrorReportingContext>(
162
+ reporter: ErrorReportingHooksOptions<Ctx>["reporter"],
163
+ args: ErrorReportingHookArgs<Ctx>,
164
+ ): Promise<ErrorReporterPort | undefined> {
165
+ if (typeof reporter === "function") {
166
+ return reporter(args);
167
+ }
168
+
169
+ if (reporter) {
170
+ return reporter;
171
+ }
172
+
173
+ return args.ctx?.ports?.errorReporter;
174
+ }
175
+
176
+ function createDefaultReportOptions<Ctx extends ErrorReportingContext>(
177
+ args: ErrorReportingHookArgs<Ctx>,
178
+ ): ErrorReportOptions {
179
+ const ctx = args.ctx;
180
+ const url = safeUrl(args.req.url);
181
+ const actor = ctx?.actor;
182
+ const tenant = ctx?.tenant;
183
+
184
+ return {
185
+ level: "error",
186
+ mechanism: "beignet.server",
187
+ handled: false,
188
+ requestId: ctx?.requestId,
189
+ traceId: ctx?.traceId,
190
+ spanId: ctx?.spanId,
191
+ parentSpanId: ctx?.parentSpanId,
192
+ traceparent: ctx?.traceparent,
193
+ user: actor?.type === "user" && actor.id ? { id: actor.id } : undefined,
194
+ tags: {
195
+ "beignet.kind": "http",
196
+ "http.method": args.req.method,
197
+ "http.route": args.contract.path,
198
+ "http.contract": args.contract.name,
199
+ },
200
+ contexts: {
201
+ request: {
202
+ method: args.req.method,
203
+ path: url?.pathname ?? args.contract.path,
204
+ route: args.contract.path,
205
+ contract: args.contract.name,
206
+ },
207
+ actor:
208
+ actor?.type || actor?.id
209
+ ? {
210
+ type: actor.type,
211
+ id: actor.id,
212
+ }
213
+ : undefined,
214
+ tenant: tenant?.id ? { id: tenant.id } : undefined,
215
+ },
216
+ };
217
+ }
218
+
219
+ function mergeReportOptions(
220
+ base: ErrorReportOptions,
221
+ custom: ErrorReportOptions | undefined,
222
+ ): ErrorReportOptions {
223
+ if (!custom) return base;
224
+
225
+ return {
226
+ ...base,
227
+ ...custom,
228
+ tags: {
229
+ ...base.tags,
230
+ ...custom.tags,
231
+ },
232
+ contexts: {
233
+ ...base.contexts,
234
+ ...custom.contexts,
235
+ },
236
+ extra: {
237
+ ...base.extra,
238
+ ...custom.extra,
239
+ },
240
+ };
241
+ }
242
+
243
+ function safeUrl(value: string): URL | undefined {
244
+ try {
245
+ return new URL(value);
246
+ } catch {
247
+ return undefined;
248
+ }
249
+ }
@@ -16,6 +16,14 @@ export {
16
16
  type CorsConfig,
17
17
  createCorsHooks,
18
18
  } from "./cors.js";
19
+ export {
20
+ createErrorReportingHooks,
21
+ type ErrorReporterResolver,
22
+ type ErrorReportingContext,
23
+ type ErrorReportingHookArgs,
24
+ type ErrorReportingHooksOptions,
25
+ shouldReportServerError,
26
+ } from "./error-reporting.js";
19
27
  export {
20
28
  defaultMapErrorToResponse,
21
29
  type ErrorMappingConfig,
@@ -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 {
@@ -2381,6 +2396,11 @@ export function contractsFromRoutes<
2381
2396
  * ```
2382
2397
  */
2383
2398
  export function defineRouteGroup<Ctx>(): RouteGroupBuilder<Ctx>;
2399
+ /**
2400
+ * @deprecated Prefer `defineRouteGroup<Ctx>()({ ... })`. TypeScript cannot
2401
+ * infer the route tuple generic when callers provide only `Ctx`, so the direct
2402
+ * form preserves context checking but erases per-route contract output checks.
2403
+ */
2384
2404
  export function defineRouteGroup<
2385
2405
  Ctx,
2386
2406
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
@@ -223,8 +223,8 @@ export type UseCaseRouteDef<
223
223
  /**
224
224
  * Structural check that a use case accepts the context this route provides.
225
225
  *
226
- * Enforced through `run` parameter contravariance so it applies even where
227
- * contract types are erased, such as `defineRouteGroup<Ctx>({ ... })`.
226
+ * Enforced through `run` parameter contravariance so it applies even at loose
227
+ * collection boundaries where contract types are erased.
228
228
  */
229
229
  export type UseCaseAcceptsCtx<Ctx> = {
230
230
  run: (args: { ctx: Ctx; input: never }) => Promise<unknown>;
@@ -1,13 +1,23 @@
1
+ import {
2
+ createMemoryErrorReporter,
3
+ type MemoryErrorReporterPort,
4
+ } from "../error-reporting/index.js";
5
+ import { createMemoryFlags, type MemoryFlagsPort } from "../flags/index.js";
1
6
  import {
2
7
  createMemoryIdempotencyStore,
3
8
  type MemoryIdempotencyStore,
4
9
  } from "../idempotency/index.js";
10
+ import { createMemoryLocks, type MemoryLocksPort } from "../locks/index.js";
5
11
  import { createMemoryMailer, type MemoryMailerPort } from "../mail/index.js";
6
12
  import {
7
13
  createMemoryNotificationPort,
8
14
  type MemoryNotificationPort,
9
15
  } from "../notifications/index.js";
10
16
  import { createMemoryOutbox, type MemoryOutboxPort } from "../outbox/index.js";
17
+ import {
18
+ createMemoryPayments,
19
+ type MemoryPaymentsPort,
20
+ } from "../payments/index.js";
11
21
  import {
12
22
  type ActivityActor,
13
23
  type ActivityTenant,
@@ -54,6 +64,7 @@ import type {
54
64
  AnyServiceProvider,
55
65
  ProviderSetupResult,
56
66
  } from "../providers/provider.js";
67
+ import { createMemorySearch, type MemorySearchPort } from "../search/index.js";
57
68
  import { createAmbientAuditLog } from "../server/audit-context.js";
58
69
  import {
59
70
  clearActiveRequestContext,
@@ -89,11 +100,19 @@ export interface CommonTestPorts<TxPorts = AnyPorts> {
89
100
  * Recording event bus.
90
101
  */
91
102
  eventBus: EventBusPort;
103
+ /**
104
+ * In-memory error reporter.
105
+ */
106
+ errorReporter: MemoryErrorReporterPort;
92
107
  /**
93
108
  * Authorization gate. Apps with real policies should provide their own gate
94
109
  * through `overrides`.
95
110
  */
96
111
  gate: GatePort<unknown, []>;
112
+ /**
113
+ * In-memory feature flags.
114
+ */
115
+ flags: MemoryFlagsPort;
97
116
  /**
98
117
  * In-memory idempotency store.
99
118
  */
@@ -110,6 +129,10 @@ export interface CommonTestPorts<TxPorts = AnyPorts> {
110
129
  * No-op logger.
111
130
  */
112
131
  logger: LoggerPort;
132
+ /**
133
+ * In-memory lease-backed locks.
134
+ */
135
+ locks: MemoryLocksPort;
113
136
  /**
114
137
  * In-memory mailer.
115
138
  */
@@ -122,6 +145,14 @@ export interface CommonTestPorts<TxPorts = AnyPorts> {
122
145
  * In-memory outbox.
123
146
  */
124
147
  outbox: MemoryOutboxPort;
148
+ /**
149
+ * In-memory payments port.
150
+ */
151
+ payments: MemoryPaymentsPort;
152
+ /**
153
+ * In-memory search port.
154
+ */
155
+ search: MemorySearchPort;
125
156
  /**
126
157
  * In-memory rate limiter.
127
158
  */
@@ -159,6 +190,10 @@ export interface TestPortsFixture<
159
190
  * Recorded events published through `eventBus`.
160
191
  */
161
192
  events: RecordedEvent[];
193
+ /**
194
+ * Memory error reporter, exposed for assertions.
195
+ */
196
+ errorReporter: MemoryErrorReporterPort;
162
197
  /**
163
198
  * Recording job dispatcher port.
164
199
  */
@@ -179,6 +214,10 @@ export interface TestPortsFixture<
179
214
  * Memory outbox, exposed for durable workflow assertions.
180
215
  */
181
216
  outbox: MemoryOutboxPort;
217
+ /**
218
+ * Memory payments port.
219
+ */
220
+ payments: MemoryPaymentsPort;
182
221
  /**
183
222
  * Memory storage port.
184
223
  */
@@ -191,6 +230,10 @@ export interface TestPortsFixture<
191
230
  * Test cache port.
192
231
  */
193
232
  cache: CachePort;
233
+ /**
234
+ * Test feature flags port.
235
+ */
236
+ flags: MemoryFlagsPort;
194
237
  /**
195
238
  * Test rate-limit port.
196
239
  */
@@ -199,6 +242,14 @@ export interface TestPortsFixture<
199
242
  * Test logger port.
200
243
  */
201
244
  logger: LoggerPort;
245
+ /**
246
+ * Test locks port.
247
+ */
248
+ locks: MemoryLocksPort;
249
+ /**
250
+ * Test search port.
251
+ */
252
+ search: MemorySearchPort;
202
253
  /**
203
254
  * Test clock port.
204
255
  */
@@ -323,14 +374,22 @@ export function createTestPorts<
323
374
  const cache = createMemoryCache();
324
375
  const clock = options.clock ?? createFrozenClock();
325
376
  const { bus: eventBus, events } = createRecordingEventBus();
377
+ const errorReporter = createMemoryErrorReporter();
378
+ const flags = createMemoryFlags();
326
379
  const { jobs, dispatchedJobs } = createRecordingJobDispatcher();
327
380
  const idempotency = createMemoryIdempotencyStore();
328
381
  const ids = options.ids ?? createUuidIdGenerator();
329
382
  const logger = createNoopLogger();
383
+ const locks = createMemoryLocks({
384
+ now: () => clock.now(),
385
+ sleep: createClockAwareSleep(clock),
386
+ });
330
387
  const mailer = createMemoryMailer();
331
388
  const notifications = createMemoryNotificationPort();
332
389
  const outbox = createMemoryOutbox();
390
+ const payments = createMemoryPayments();
333
391
  const rateLimit = createMemoryRateLimiter();
392
+ const search = createMemorySearch();
334
393
  const storage = createMemoryStorage();
335
394
 
336
395
  const defaultPorts = {
@@ -338,15 +397,20 @@ export function createTestPorts<
338
397
  cache,
339
398
  clock,
340
399
  eventBus,
400
+ errorReporter,
401
+ flags,
341
402
  gate: createGate({ policies: [] }),
342
403
  idempotency,
343
404
  ids,
344
405
  jobs,
345
406
  logger,
407
+ locks,
346
408
  mailer,
347
409
  notifications,
348
410
  outbox,
411
+ payments,
349
412
  rateLimit,
413
+ search,
350
414
  storage,
351
415
  };
352
416
  const overrides = completeTestPortOverrides(options.overrides);
@@ -409,16 +473,21 @@ export function createTestPorts<
409
473
  audit,
410
474
  eventBus,
411
475
  events,
476
+ errorReporter,
412
477
  jobs,
413
478
  dispatchedJobs,
414
479
  mailer,
415
480
  notifications,
416
481
  outbox,
482
+ payments,
417
483
  storage,
418
484
  idempotency,
419
485
  cache,
486
+ flags,
420
487
  rateLimit,
421
488
  logger,
489
+ locks,
490
+ search,
422
491
  clock,
423
492
  ids,
424
493
  uow,
@@ -432,6 +501,19 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
432
501
  return proto === Object.prototype || proto === null;
433
502
  }
434
503
 
504
+ function createClockAwareSleep(
505
+ clock: ClockPort,
506
+ ): (ms: number) => Promise<void> {
507
+ const maybeFrozenClock = clock as Partial<{ advance(ms: number): void }>;
508
+ if (typeof maybeFrozenClock.advance === "function") {
509
+ return async (ms) => {
510
+ maybeFrozenClock.advance?.(ms);
511
+ };
512
+ }
513
+
514
+ return (ms) => new Promise((resolve) => setTimeout(resolve, ms));
515
+ }
516
+
435
517
  function createMissingTestPortMember(
436
518
  portKey: string,
437
519
  member: string,
@@ -799,7 +881,7 @@ export interface TestContextFixture<Ctx, Ports extends AnyPorts>
799
881
 
800
882
  /**
801
883
  * Create a one-call test context fixture for jobs, listeners, schedules,
802
- * notifications, tasks, and use-case tests.
884
+ * notifications, payments, tasks, and use-case tests.
803
885
  *
804
886
  * The fixture builds common memory ports through `createTestPorts(...)`,
805
887
  * assembles an app context with actor, tenant, request ID, trace ID, auth,