@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
@@ -15,19 +15,29 @@
15
15
  * - `AuthPort` – interface for request authentication implementations
16
16
  * - `AuditLogPort` – interface for audit/activity log implementations
17
17
  * - `ClockPort` – interface for deterministic time
18
+ * - `EntitlementsPort` – interface for product access checks
18
19
  * - `IdGeneratorPort` – interface for deterministic id generation
19
20
  * - `LoggerPort` – interface for structured application logging
20
21
  * - `GatePort` – interface for application authorization policies
21
22
  * - `RateLimitPort` – interface for rate limiting implementations
22
23
  * - `CachePort` – interface for cache implementations
23
24
  * - `StoragePort` – interface for object/file storage implementations
25
+ * - `FlagsPort` – interface for feature flag evaluation implementations
26
+ * - `ErrorReporterPort` – interface for production error reporting
27
+ * - `LocksPort` – interface for lease-backed distributed locks
24
28
  *
25
29
  * Dedicated framework areas own their capability-specific APIs:
30
+ * - `@beignet/core/entitlements` owns product access helpers and test adapters
31
+ * - `@beignet/core/error-reporting` owns error reporting helpers and test adapters
32
+ * - `@beignet/core/flags` owns feature flag helpers and test adapters
26
33
  * - `@beignet/core/idempotency` owns idempotency helpers and test adapters
34
+ * - `@beignet/core/locks` owns lease-backed lock helpers and test adapters
27
35
  * - `@beignet/core/mail` owns `MailerPort` and mail test adapters
28
36
  * - `@beignet/core/notifications` owns notification helpers and test adapters
29
37
  * - `@beignet/core/outbox` owns durable outbox helpers and test adapters
38
+ * - `@beignet/core/payments` owns `PaymentsPort` and payment test adapters
30
39
  * - `@beignet/core/schedules` owns schedule definitions and runners
40
+ * - `@beignet/core/webhooks` owns inbound webhook definitions and verifiers
31
41
  */
32
42
 
33
43
  import { createUnboundPort } from "./unbound.js";
@@ -136,6 +146,89 @@ export interface PortsContext<P extends AnyPorts = AnyPorts> {
136
146
  ports: P;
137
147
  }
138
148
 
149
+ /**
150
+ * Entitlement port exports.
151
+ */
152
+ export type {
153
+ CreateEntitlementsOptions,
154
+ CreateStaticEntitlementsOptions,
155
+ EntitlementAllowedDecision,
156
+ EntitlementCheckInput,
157
+ EntitlementDecision,
158
+ EntitlementDeniedDecision,
159
+ EntitlementKey,
160
+ EntitlementResolver,
161
+ EntitlementResolverResult,
162
+ EntitlementSubject,
163
+ EntitlementsContext,
164
+ EntitlementsPort,
165
+ RequireEntitlementOptions,
166
+ StaticEntitlementGrantMap,
167
+ } from "../entitlements/index.js";
168
+ /**
169
+ * Entitlement helper exports.
170
+ */
171
+ export {
172
+ allowEntitlement,
173
+ createEntitlements,
174
+ createStaticEntitlements,
175
+ denyEntitlement,
176
+ EntitlementRequiredError,
177
+ requireEntitlement,
178
+ } from "../entitlements/index.js";
179
+ /**
180
+ * Error reporting port exports.
181
+ */
182
+ export type {
183
+ ErrorReportContext,
184
+ ErrorReporterFlushOptions,
185
+ ErrorReporterPort,
186
+ ErrorReportJsonValue,
187
+ ErrorReportLevel,
188
+ ErrorReportOptions,
189
+ ErrorReportResult,
190
+ ErrorReportTags,
191
+ ErrorReportUser,
192
+ MemoryErrorReport,
193
+ MemoryErrorReporterPort,
194
+ MemoryReportedException,
195
+ MemoryReportedMessage,
196
+ } from "../error-reporting/index.js";
197
+ /**
198
+ * Error reporting helper exports.
199
+ */
200
+ export {
201
+ createMemoryErrorReporter,
202
+ createNoopErrorReporter,
203
+ reportException,
204
+ reportMessage,
205
+ } from "../error-reporting/index.js";
206
+ /**
207
+ * Feature flag port exports.
208
+ */
209
+ export type {
210
+ FlagDef,
211
+ FlagEvaluationContext,
212
+ FlagEvaluationDetails,
213
+ FlagEvaluationOptions,
214
+ FlagExposureOptions,
215
+ FlagsPort,
216
+ FlagTrackOptions,
217
+ FlagValue,
218
+ MemoryFlagsPort,
219
+ } from "../flags/index.js";
220
+ /**
221
+ * Feature flag helper exports.
222
+ */
223
+ export {
224
+ createMemoryFlags,
225
+ createStaticFlags,
226
+ defineFlag,
227
+ defineFlagRegistry,
228
+ defineFlags,
229
+ evaluateFlag,
230
+ getFlagDetails,
231
+ } from "../flags/index.js";
139
232
  /**
140
233
  * Idempotency port exports.
141
234
  */
@@ -146,6 +239,32 @@ export type {
146
239
  IdempotencyReservation,
147
240
  IdempotencyReserveInput,
148
241
  } from "../idempotency/index.js";
242
+ /**
243
+ * Lease-backed lock port exports.
244
+ */
245
+ export type {
246
+ CreateMemoryLocksOptions,
247
+ LeaseAcquireOptions,
248
+ LeaseAcquireResult,
249
+ LeaseHandle,
250
+ LeaseMetadata,
251
+ LeaseRenewOptions,
252
+ LocksPort,
253
+ MemoryLeaseRecord,
254
+ MemoryLocksPort,
255
+ MemoryLocksProviderOptions,
256
+ MemoryLocksProviderPorts,
257
+ } from "../locks/index.js";
258
+ /**
259
+ * Lease-backed lock helper exports.
260
+ */
261
+ export {
262
+ acquireLease,
263
+ createMemoryLocks,
264
+ createMemoryLocksProvider,
265
+ LeaseOptionsError,
266
+ withLease,
267
+ } from "../locks/index.js";
149
268
  /**
150
269
  * Notification port exports.
151
270
  */
@@ -171,6 +290,111 @@ export type {
171
290
  OutboxMessageStatus,
172
291
  OutboxPort,
173
292
  } from "../outbox/index.js";
293
+ /**
294
+ * Payments port exports.
295
+ */
296
+ export type {
297
+ BillingPortalSession,
298
+ CheckoutSession,
299
+ CreateBillingPortalSessionInput,
300
+ CreateCheckoutSessionInput,
301
+ CreateRefundInput,
302
+ MemoryBillingPortalSession,
303
+ MemoryCheckoutSession,
304
+ MemoryPaymentsPort,
305
+ MemoryRefund,
306
+ PaymentCheckoutLineItem,
307
+ PaymentCheckoutMode,
308
+ PaymentMetadata,
309
+ PaymentRefundReason,
310
+ PaymentsPort,
311
+ PaymentWebhookEvent,
312
+ PaymentWebhookRawBody,
313
+ Refund,
314
+ VerifyPaymentWebhookInput,
315
+ } from "../payments/index.js";
316
+ /**
317
+ * Payments helper exports.
318
+ */
319
+ export {
320
+ createMemoryPayments,
321
+ createMemoryPaymentsProvider,
322
+ PaymentProviderError,
323
+ } from "../payments/index.js";
324
+ /**
325
+ * Search port exports.
326
+ *
327
+ * Search has a dedicated subpath. Re-export the common app port types and
328
+ * helpers here so apps can import shared port surfaces from one place.
329
+ */
330
+ export type {
331
+ DefineSearchIndexOptions,
332
+ MemorySearchIndexState,
333
+ MemorySearchPort,
334
+ MemorySearchProviderOptions,
335
+ MemorySearchProviderPorts,
336
+ SearchDeleteResult,
337
+ SearchDocument,
338
+ SearchDocumentBase,
339
+ SearchFilters,
340
+ SearchFilterValue,
341
+ SearchIndexDef,
342
+ SearchIndexResult,
343
+ SearchPort,
344
+ SearchPrimitive,
345
+ SearchQuery,
346
+ SearchResultPage,
347
+ SearchResults,
348
+ SearchSort,
349
+ SearchValue,
350
+ } from "../search/index.js";
351
+ /**
352
+ * Search helper exports.
353
+ */
354
+ export {
355
+ createMemorySearch,
356
+ createMemorySearchProvider,
357
+ defineSearchIndex,
358
+ indexSearchDocuments,
359
+ SearchOptionsError,
360
+ searchDocuments,
361
+ } from "../search/index.js";
362
+ /**
363
+ * Webhook exports.
364
+ *
365
+ * Webhooks have a dedicated subpath. Re-export the common app-facing
366
+ * definitions and helpers here so production concerns can be imported from one
367
+ * shared port toolbox when useful.
368
+ */
369
+ export type {
370
+ CreateHmacWebhookVerifierOptions,
371
+ CreateMemoryWebhookVerifierOptions,
372
+ DefineWebhookOptions,
373
+ InferSchemaOutput,
374
+ InferWebhookEvent,
375
+ MemoryWebhookVerifier,
376
+ VerifyWebhookInput,
377
+ VerifyWebhookOptions,
378
+ WebhookDef,
379
+ WebhookEvent,
380
+ WebhookEventSchemas,
381
+ WebhookHeaders,
382
+ WebhookRawBody,
383
+ WebhookVerifier,
384
+ } from "../webhooks/index.js";
385
+ /**
386
+ * Webhook helper exports.
387
+ */
388
+ export {
389
+ createHmacWebhookVerifier,
390
+ createMemoryWebhookVerifier,
391
+ defineWebhook,
392
+ parseWebhookEvent,
393
+ verifyWebhook,
394
+ WebhookOptionsError,
395
+ WebhookValidationError,
396
+ WebhookVerificationError,
397
+ } from "../webhooks/index.js";
174
398
  /**
175
399
  * Audit log port exports.
176
400
  */
@@ -299,10 +523,17 @@ export type {
299
523
  GateAllowedDecision,
300
524
  GateContext,
301
525
  GateDecision,
526
+ GateDecisionObservation,
527
+ GateDecisionObserver,
528
+ GateDecisionSource,
302
529
  GateDeniedDecision,
303
530
  GateDenyHandler,
304
531
  GatePolicyResult,
305
532
  GatePort,
533
+ PolicyBatch,
534
+ PolicyBatchBooleanMap,
535
+ PolicyBatchCheck,
536
+ PolicyBatchDecisionMap,
306
537
  PolicyContextFromDefinitions,
307
538
  PolicyDefinition,
308
539
  PolicyMapFromDefinitions,
@@ -77,6 +77,54 @@ export type PolicySubjectArgs<TResolver> = TResolver extends (
77
77
  : []
78
78
  : [];
79
79
 
80
+ /**
81
+ * One authorization check accepted by batch gate APIs.
82
+ */
83
+ export type PolicyBatchCheck<TPolicies extends readonly PolicyDefinition[]> = {
84
+ [TAbility in keyof PolicyMapFromDefinitions<TPolicies> & string]: readonly [
85
+ ability: TAbility,
86
+ ...subject: PolicySubjectArgs<
87
+ PolicyMapFromDefinitions<TPolicies>[TAbility]
88
+ >,
89
+ ];
90
+ }[keyof PolicyMapFromDefinitions<TPolicies> & string];
91
+
92
+ /**
93
+ * Keyed authorization checks accepted by `inspectMany(...)` and `canMany(...)`.
94
+ */
95
+ export type PolicyBatch<TPolicies extends readonly PolicyDefinition[]> = Record<
96
+ string,
97
+ PolicyBatchCheck<TPolicies>
98
+ >;
99
+
100
+ /**
101
+ * Full decision map returned by `inspectMany(...)`.
102
+ */
103
+ export type PolicyBatchDecisionMap<
104
+ TBatch extends PolicyBatch<readonly PolicyDefinition[]>,
105
+ > = {
106
+ [TKey in keyof TBatch]: GateDecision;
107
+ };
108
+
109
+ /**
110
+ * Boolean decision map returned by `canMany(...)`.
111
+ */
112
+ export type PolicyBatchBooleanMap<
113
+ TBatch extends PolicyBatch<readonly PolicyDefinition[]>,
114
+ > = {
115
+ [TKey in keyof TBatch]: boolean;
116
+ };
117
+
118
+ /**
119
+ * Gate method that produced a policy decision observation.
120
+ */
121
+ export type GateDecisionSource =
122
+ | "can"
123
+ | "inspect"
124
+ | "authorize"
125
+ | "canMany"
126
+ | "inspectMany";
127
+
80
128
  type UnionToIntersection<T> = (
81
129
  T extends unknown
82
130
  ? (value: T) => void
@@ -126,6 +174,12 @@ export type BoundGate<TPolicies extends readonly PolicyDefinition[]> = {
126
174
  ability: TAbility,
127
175
  ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
128
176
  ): Promise<boolean>;
177
+ /**
178
+ * Return keyed boolean decisions for several abilities.
179
+ */
180
+ canMany<const TBatch extends PolicyBatch<TPolicies>>(
181
+ checks: TBatch,
182
+ ): Promise<PolicyBatchBooleanMap<TBatch>>;
129
183
  /**
130
184
  * Return the full allow/deny decision without throwing.
131
185
  */
@@ -133,6 +187,12 @@ export type BoundGate<TPolicies extends readonly PolicyDefinition[]> = {
133
187
  ability: TAbility,
134
188
  ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
135
189
  ): Promise<GateDecision>;
190
+ /**
191
+ * Return keyed allow/deny decisions for several abilities.
192
+ */
193
+ inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
194
+ checks: TBatch,
195
+ ): Promise<PolicyBatchDecisionMap<TBatch>>;
136
196
  /**
137
197
  * Return an allowed decision or throw for denied abilities.
138
198
  */
@@ -193,6 +253,13 @@ export type GatePort<
193
253
  ability: TAbility,
194
254
  ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
195
255
  ): Promise<boolean>;
256
+ /**
257
+ * Return keyed boolean decisions for several abilities.
258
+ */
259
+ canMany<const TBatch extends PolicyBatch<TPolicies>>(
260
+ ctx: TContext,
261
+ checks: TBatch,
262
+ ): Promise<PolicyBatchBooleanMap<TBatch>>;
196
263
  /**
197
264
  * Return the full allow/deny decision for a context without throwing.
198
265
  */
@@ -201,6 +268,13 @@ export type GatePort<
201
268
  ability: TAbility,
202
269
  ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
203
270
  ): Promise<GateDecision>;
271
+ /**
272
+ * Return keyed allow/deny decisions for several abilities.
273
+ */
274
+ inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
275
+ ctx: TContext,
276
+ checks: TBatch,
277
+ ): Promise<PolicyBatchDecisionMap<TBatch>>;
204
278
  /**
205
279
  * Return an allowed decision or throw for denied abilities.
206
280
  */
@@ -225,6 +299,63 @@ export type GateDenyHandler<TContext> = (
225
299
  },
226
300
  ) => MaybePromise<Error | undefined>;
227
301
 
302
+ /**
303
+ * Best-effort policy decision observation emitted by a gate.
304
+ *
305
+ * Observers are for diagnostics and audit-style integrations only. They do not
306
+ * participate in authorization control flow and thrown/rejected observer errors
307
+ * are ignored.
308
+ */
309
+ export type GateDecisionObservation<TContext> = {
310
+ /**
311
+ * Gate method that produced the decision.
312
+ */
313
+ source: GateDecisionSource;
314
+ /**
315
+ * Key from a batch input object, when the decision came from a batch call.
316
+ */
317
+ batchKey?: string;
318
+ /**
319
+ * Application context used for policy evaluation.
320
+ */
321
+ ctx: TContext;
322
+ /**
323
+ * Ability being evaluated.
324
+ */
325
+ ability: string;
326
+ /**
327
+ * Optional subject passed to the policy.
328
+ */
329
+ subject?: unknown;
330
+ /**
331
+ * Normalized decision, when policy evaluation returned normally.
332
+ */
333
+ decision?: GateDecision;
334
+ /**
335
+ * Error thrown by the policy resolver, when evaluation failed.
336
+ */
337
+ error?: unknown;
338
+ /**
339
+ * Policy evaluation duration, excluding observer work.
340
+ */
341
+ durationMs: number;
342
+ /**
343
+ * Correlation fields copied from the context when present.
344
+ */
345
+ requestId?: string;
346
+ traceId?: string;
347
+ spanId?: string;
348
+ parentSpanId?: string;
349
+ traceparent?: string;
350
+ };
351
+
352
+ /**
353
+ * Best-effort observer called after each gate decision or resolver error.
354
+ */
355
+ export type GateDecisionObserver<TContext> = (
356
+ observation: GateDecisionObservation<TContext>,
357
+ ) => MaybePromise<void>;
358
+
228
359
  /**
229
360
  * Options for `createGate(...)`.
230
361
  */
@@ -240,6 +371,12 @@ export type CreateGateOptions<
240
371
  * Optional mapper for denied authorization decisions.
241
372
  */
242
373
  onDeny?: GateDenyHandler<TContext>;
374
+ /**
375
+ * Optional best-effort observer for policy decisions.
376
+ *
377
+ * This hook is diagnostic only: it cannot change decisions or thrown errors.
378
+ */
379
+ onDecision?: GateDecisionObserver<TContext>;
243
380
  };
244
381
 
245
382
  /**
@@ -330,6 +467,7 @@ export function createGate<
330
467
  options: CreateGateOptions<TContext, TPolicies>,
331
468
  ): GatePort<TContext, TPolicies> {
332
469
  const registry = new Map<string, PolicyResolver>();
470
+ const observeDecision = createDecisionObserver(options.onDecision);
333
471
 
334
472
  for (const definition of options.policies) {
335
473
  for (const [ability, resolver] of Object.entries(definition.policies)) {
@@ -341,21 +479,62 @@ export function createGate<
341
479
  }
342
480
  }
343
481
 
344
- async function inspect(
482
+ async function evaluate(
345
483
  ctx: TContext,
346
484
  ability: string,
347
485
  subject?: unknown,
486
+ source: GateDecisionSource = "inspect",
487
+ batchKey?: string,
348
488
  ): Promise<GateDecision> {
489
+ const startedAt = policyNow();
349
490
  const resolver = registry.get(ability);
350
491
  if (!resolver) {
351
- return deny({
492
+ const decision = deny({
352
493
  reason: `No policy registered for "${ability}".`,
353
494
  code: "POLICY_NOT_FOUND",
354
495
  });
496
+
497
+ observeDecision({
498
+ source,
499
+ batchKey,
500
+ ctx,
501
+ ability,
502
+ subject,
503
+ decision,
504
+ durationMs: policyNow() - startedAt,
505
+ });
506
+
507
+ return decision;
355
508
  }
356
509
 
357
- const result = await resolver(ctx as never, subject as never);
358
- return normalizeDecision(result);
510
+ try {
511
+ const result = await resolver(ctx as never, subject as never);
512
+ const decision = normalizeDecision(result);
513
+
514
+ observeDecision({
515
+ source,
516
+ batchKey,
517
+ ctx,
518
+ ability,
519
+ subject,
520
+ decision,
521
+ durationMs: policyNow() - startedAt,
522
+ });
523
+
524
+ return decision;
525
+ } catch (error) {
526
+ observeDecision({
527
+ source,
528
+ batchKey,
529
+ ctx,
530
+ ability,
531
+ subject,
532
+ error,
533
+ durationMs: policyNow() - startedAt,
534
+ });
535
+
536
+ throw error;
537
+ }
359
538
  }
360
539
 
361
540
  async function authorize(
@@ -363,18 +542,44 @@ export function createGate<
363
542
  ability: string,
364
543
  subject?: unknown,
365
544
  ): Promise<GateAllowedDecision> {
366
- const decision = await inspect(ctx, ability, subject);
545
+ const decision = await evaluate(ctx, ability, subject, "authorize");
367
546
  if (decision.allowed) return decision;
368
547
 
369
548
  const thrown = await options.onDeny?.(decision, { ctx, ability, subject });
370
549
  throw thrown ?? new GateAuthorizationError(decision);
371
550
  }
372
551
 
552
+ async function inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
553
+ ctx: TContext,
554
+ checks: TBatch,
555
+ source: "canMany" | "inspectMany",
556
+ ): Promise<PolicyBatchDecisionMap<TBatch>> {
557
+ const decisions: Record<string, GateDecision> = {};
558
+
559
+ for (const [batchKey, check] of Object.entries(checks)) {
560
+ const [ability, subject] = check as unknown as readonly [
561
+ string,
562
+ unknown?,
563
+ ];
564
+ decisions[batchKey] = await evaluate(
565
+ ctx,
566
+ ability,
567
+ subject,
568
+ source,
569
+ batchKey,
570
+ );
571
+ }
572
+
573
+ return decisions as PolicyBatchDecisionMap<TBatch>;
574
+ }
575
+
373
576
  function bind(ctx: TContext): BoundGate<TPolicies> {
374
577
  return {
375
578
  can: async (ability, ...subject) => gate.can(ctx, ability, ...subject),
579
+ canMany: async (checks) => gate.canMany(ctx, checks),
376
580
  inspect: async (ability, ...subject) =>
377
581
  gate.inspect(ctx, ability, ...subject),
582
+ inspectMany: async (checks) => gate.inspectMany(ctx, checks),
378
583
  authorize: async (ability, ...subject) =>
379
584
  gate.authorize(ctx, ability, ...subject),
380
585
  };
@@ -398,10 +603,22 @@ export function createGate<
398
603
  bind,
399
604
  attach,
400
605
  async can(ctx, ability, ...subject) {
401
- return (await inspect(ctx, ability, firstSubject(subject))).allowed;
606
+ return (await evaluate(ctx, ability, firstSubject(subject), "can"))
607
+ .allowed;
608
+ },
609
+ async canMany(ctx, checks) {
610
+ const decisions = await inspectMany(ctx, checks, "canMany");
611
+ const result: Record<string, boolean> = {};
612
+
613
+ for (const [key, decision] of Object.entries(decisions)) {
614
+ result[key] = decision.allowed;
615
+ }
616
+
617
+ return result as PolicyBatchBooleanMap<typeof checks>;
402
618
  },
403
619
  inspect: async (ctx, ability, ...subject) =>
404
- inspect(ctx, ability, firstSubject(subject)),
620
+ evaluate(ctx, ability, firstSubject(subject), "inspect"),
621
+ inspectMany: async (ctx, checks) => inspectMany(ctx, checks, "inspectMany"),
405
622
  authorize: async (ctx, ability, ...subject) =>
406
623
  authorize(ctx, ability, firstSubject(subject)),
407
624
  };
@@ -420,3 +637,51 @@ function normalizeDecision(result: GatePolicyResult): GateDecision {
420
637
  function firstSubject(subject: readonly unknown[]): unknown {
421
638
  return subject[0];
422
639
  }
640
+
641
+ function policyNow(): number {
642
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
643
+ }
644
+
645
+ function contextStringField(ctx: unknown, field: string): string | undefined {
646
+ if (!ctx || typeof ctx !== "object") return undefined;
647
+ const value = (ctx as Record<string, unknown>)[field];
648
+ return typeof value === "string" ? value : undefined;
649
+ }
650
+
651
+ function enrichObservation<TContext>(
652
+ observation: GateDecisionObservation<TContext>,
653
+ ): GateDecisionObservation<TContext> {
654
+ return {
655
+ ...observation,
656
+ requestId: contextStringField(observation.ctx, "requestId"),
657
+ traceId: contextStringField(observation.ctx, "traceId"),
658
+ spanId: contextStringField(observation.ctx, "spanId"),
659
+ parentSpanId: contextStringField(observation.ctx, "parentSpanId"),
660
+ traceparent: contextStringField(observation.ctx, "traceparent"),
661
+ };
662
+ }
663
+
664
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
665
+ return (
666
+ value !== null &&
667
+ (typeof value === "object" || typeof value === "function") &&
668
+ typeof (value as { then?: unknown }).then === "function"
669
+ );
670
+ }
671
+
672
+ function createDecisionObserver<TContext>(
673
+ observer: GateDecisionObserver<TContext> | undefined,
674
+ ): (observation: GateDecisionObservation<TContext>) => void {
675
+ return (observation) => {
676
+ if (!observer) return;
677
+
678
+ try {
679
+ const result = observer(enrichObservation(observation));
680
+ if (isPromiseLike(result)) {
681
+ Promise.resolve(result).catch(() => undefined);
682
+ }
683
+ } catch {
684
+ // Observers are diagnostic only and must not affect authorization.
685
+ }
686
+ };
687
+ }