@beignet/core 0.0.12 → 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.
- package/CHANGELOG.md +21 -0
- package/README.md +159 -9
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +29 -1
- package/dist/client/client.js.map +1 -1
- package/dist/entitlements/index.d.ts +234 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +172 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/error-reporting/index.d.ts +131 -0
- package/dist/error-reporting/index.d.ts.map +1 -0
- package/dist/error-reporting/index.js +112 -0
- package/dist/error-reporting/index.js.map +1 -0
- package/dist/flags/index.d.ts +293 -0
- package/dist/flags/index.d.ts.map +1 -0
- package/dist/flags/index.js +204 -0
- package/dist/flags/index.js.map +1 -0
- package/dist/locks/index.d.ts +155 -0
- package/dist/locks/index.d.ts.map +1 -0
- package/dist/locks/index.js +248 -0
- package/dist/locks/index.js.map +1 -0
- package/dist/payments/index.d.ts +430 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +230 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/ports/index.d.ts +61 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +33 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +104 -0
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +102 -7
- package/dist/ports/policy.js.map +1 -1
- package/dist/search/index.d.ts +175 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +344 -0
- package/dist/search/index.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +9 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +46 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +33 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +25 -1
- package/src/client/client.ts +33 -1
- package/src/entitlements/index.ts +479 -0
- package/src/error-reporting/index.ts +273 -0
- package/src/flags/index.ts +608 -0
- package/src/locks/index.ts +440 -0
- package/src/payments/index.ts +699 -0
- package/src/ports/index.ts +194 -0
- package/src/ports/policy.ts +272 -7
- package/src/search/index.ts +632 -0
- package/src/server/server.ts +15 -0
- package/src/testing/index.ts +83 -1
package/src/ports/index.ts
CHANGED
|
@@ -15,18 +15,27 @@
|
|
|
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
|
|
31
40
|
*/
|
|
32
41
|
|
|
@@ -136,6 +145,89 @@ export interface PortsContext<P extends AnyPorts = AnyPorts> {
|
|
|
136
145
|
ports: P;
|
|
137
146
|
}
|
|
138
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Entitlement port exports.
|
|
150
|
+
*/
|
|
151
|
+
export type {
|
|
152
|
+
CreateEntitlementsOptions,
|
|
153
|
+
CreateStaticEntitlementsOptions,
|
|
154
|
+
EntitlementAllowedDecision,
|
|
155
|
+
EntitlementCheckInput,
|
|
156
|
+
EntitlementDecision,
|
|
157
|
+
EntitlementDeniedDecision,
|
|
158
|
+
EntitlementKey,
|
|
159
|
+
EntitlementResolver,
|
|
160
|
+
EntitlementResolverResult,
|
|
161
|
+
EntitlementSubject,
|
|
162
|
+
EntitlementsContext,
|
|
163
|
+
EntitlementsPort,
|
|
164
|
+
RequireEntitlementOptions,
|
|
165
|
+
StaticEntitlementGrantMap,
|
|
166
|
+
} from "../entitlements/index.js";
|
|
167
|
+
/**
|
|
168
|
+
* Entitlement helper exports.
|
|
169
|
+
*/
|
|
170
|
+
export {
|
|
171
|
+
allowEntitlement,
|
|
172
|
+
createEntitlements,
|
|
173
|
+
createStaticEntitlements,
|
|
174
|
+
denyEntitlement,
|
|
175
|
+
EntitlementRequiredError,
|
|
176
|
+
requireEntitlement,
|
|
177
|
+
} from "../entitlements/index.js";
|
|
178
|
+
/**
|
|
179
|
+
* Error reporting port exports.
|
|
180
|
+
*/
|
|
181
|
+
export type {
|
|
182
|
+
ErrorReportContext,
|
|
183
|
+
ErrorReporterFlushOptions,
|
|
184
|
+
ErrorReporterPort,
|
|
185
|
+
ErrorReportJsonValue,
|
|
186
|
+
ErrorReportLevel,
|
|
187
|
+
ErrorReportOptions,
|
|
188
|
+
ErrorReportResult,
|
|
189
|
+
ErrorReportTags,
|
|
190
|
+
ErrorReportUser,
|
|
191
|
+
MemoryErrorReport,
|
|
192
|
+
MemoryErrorReporterPort,
|
|
193
|
+
MemoryReportedException,
|
|
194
|
+
MemoryReportedMessage,
|
|
195
|
+
} from "../error-reporting/index.js";
|
|
196
|
+
/**
|
|
197
|
+
* Error reporting helper exports.
|
|
198
|
+
*/
|
|
199
|
+
export {
|
|
200
|
+
createMemoryErrorReporter,
|
|
201
|
+
createNoopErrorReporter,
|
|
202
|
+
reportException,
|
|
203
|
+
reportMessage,
|
|
204
|
+
} from "../error-reporting/index.js";
|
|
205
|
+
/**
|
|
206
|
+
* Feature flag port exports.
|
|
207
|
+
*/
|
|
208
|
+
export type {
|
|
209
|
+
FlagDef,
|
|
210
|
+
FlagEvaluationContext,
|
|
211
|
+
FlagEvaluationDetails,
|
|
212
|
+
FlagEvaluationOptions,
|
|
213
|
+
FlagExposureOptions,
|
|
214
|
+
FlagsPort,
|
|
215
|
+
FlagTrackOptions,
|
|
216
|
+
FlagValue,
|
|
217
|
+
MemoryFlagsPort,
|
|
218
|
+
} from "../flags/index.js";
|
|
219
|
+
/**
|
|
220
|
+
* Feature flag helper exports.
|
|
221
|
+
*/
|
|
222
|
+
export {
|
|
223
|
+
createMemoryFlags,
|
|
224
|
+
createStaticFlags,
|
|
225
|
+
defineFlag,
|
|
226
|
+
defineFlagRegistry,
|
|
227
|
+
defineFlags,
|
|
228
|
+
evaluateFlag,
|
|
229
|
+
getFlagDetails,
|
|
230
|
+
} from "../flags/index.js";
|
|
139
231
|
/**
|
|
140
232
|
* Idempotency port exports.
|
|
141
233
|
*/
|
|
@@ -146,6 +238,32 @@ export type {
|
|
|
146
238
|
IdempotencyReservation,
|
|
147
239
|
IdempotencyReserveInput,
|
|
148
240
|
} from "../idempotency/index.js";
|
|
241
|
+
/**
|
|
242
|
+
* Lease-backed lock port exports.
|
|
243
|
+
*/
|
|
244
|
+
export type {
|
|
245
|
+
CreateMemoryLocksOptions,
|
|
246
|
+
LeaseAcquireOptions,
|
|
247
|
+
LeaseAcquireResult,
|
|
248
|
+
LeaseHandle,
|
|
249
|
+
LeaseMetadata,
|
|
250
|
+
LeaseRenewOptions,
|
|
251
|
+
LocksPort,
|
|
252
|
+
MemoryLeaseRecord,
|
|
253
|
+
MemoryLocksPort,
|
|
254
|
+
MemoryLocksProviderOptions,
|
|
255
|
+
MemoryLocksProviderPorts,
|
|
256
|
+
} from "../locks/index.js";
|
|
257
|
+
/**
|
|
258
|
+
* Lease-backed lock helper exports.
|
|
259
|
+
*/
|
|
260
|
+
export {
|
|
261
|
+
acquireLease,
|
|
262
|
+
createMemoryLocks,
|
|
263
|
+
createMemoryLocksProvider,
|
|
264
|
+
LeaseOptionsError,
|
|
265
|
+
withLease,
|
|
266
|
+
} from "../locks/index.js";
|
|
149
267
|
/**
|
|
150
268
|
* Notification port exports.
|
|
151
269
|
*/
|
|
@@ -171,6 +289,75 @@ export type {
|
|
|
171
289
|
OutboxMessageStatus,
|
|
172
290
|
OutboxPort,
|
|
173
291
|
} from "../outbox/index.js";
|
|
292
|
+
/**
|
|
293
|
+
* Payments port exports.
|
|
294
|
+
*/
|
|
295
|
+
export type {
|
|
296
|
+
BillingPortalSession,
|
|
297
|
+
CheckoutSession,
|
|
298
|
+
CreateBillingPortalSessionInput,
|
|
299
|
+
CreateCheckoutSessionInput,
|
|
300
|
+
CreateRefundInput,
|
|
301
|
+
MemoryBillingPortalSession,
|
|
302
|
+
MemoryCheckoutSession,
|
|
303
|
+
MemoryPaymentsPort,
|
|
304
|
+
MemoryRefund,
|
|
305
|
+
PaymentCheckoutLineItem,
|
|
306
|
+
PaymentCheckoutMode,
|
|
307
|
+
PaymentMetadata,
|
|
308
|
+
PaymentRefundReason,
|
|
309
|
+
PaymentsPort,
|
|
310
|
+
PaymentWebhookEvent,
|
|
311
|
+
PaymentWebhookRawBody,
|
|
312
|
+
Refund,
|
|
313
|
+
VerifyPaymentWebhookInput,
|
|
314
|
+
} from "../payments/index.js";
|
|
315
|
+
/**
|
|
316
|
+
* Payments helper exports.
|
|
317
|
+
*/
|
|
318
|
+
export {
|
|
319
|
+
createMemoryPayments,
|
|
320
|
+
createMemoryPaymentsProvider,
|
|
321
|
+
PaymentProviderError,
|
|
322
|
+
} from "../payments/index.js";
|
|
323
|
+
/**
|
|
324
|
+
* Search port exports.
|
|
325
|
+
*
|
|
326
|
+
* Search has a dedicated subpath. Re-export the common app port types and
|
|
327
|
+
* helpers here so apps can import shared port surfaces from one place.
|
|
328
|
+
*/
|
|
329
|
+
export type {
|
|
330
|
+
DefineSearchIndexOptions,
|
|
331
|
+
MemorySearchIndexState,
|
|
332
|
+
MemorySearchPort,
|
|
333
|
+
MemorySearchProviderOptions,
|
|
334
|
+
MemorySearchProviderPorts,
|
|
335
|
+
SearchDeleteResult,
|
|
336
|
+
SearchDocument,
|
|
337
|
+
SearchDocumentBase,
|
|
338
|
+
SearchFilters,
|
|
339
|
+
SearchFilterValue,
|
|
340
|
+
SearchIndexDef,
|
|
341
|
+
SearchIndexResult,
|
|
342
|
+
SearchPort,
|
|
343
|
+
SearchPrimitive,
|
|
344
|
+
SearchQuery,
|
|
345
|
+
SearchResultPage,
|
|
346
|
+
SearchResults,
|
|
347
|
+
SearchSort,
|
|
348
|
+
SearchValue,
|
|
349
|
+
} from "../search/index.js";
|
|
350
|
+
/**
|
|
351
|
+
* Search helper exports.
|
|
352
|
+
*/
|
|
353
|
+
export {
|
|
354
|
+
createMemorySearch,
|
|
355
|
+
createMemorySearchProvider,
|
|
356
|
+
defineSearchIndex,
|
|
357
|
+
indexSearchDocuments,
|
|
358
|
+
SearchOptionsError,
|
|
359
|
+
searchDocuments,
|
|
360
|
+
} from "../search/index.js";
|
|
174
361
|
/**
|
|
175
362
|
* Audit log port exports.
|
|
176
363
|
*/
|
|
@@ -299,10 +486,17 @@ export type {
|
|
|
299
486
|
GateAllowedDecision,
|
|
300
487
|
GateContext,
|
|
301
488
|
GateDecision,
|
|
489
|
+
GateDecisionObservation,
|
|
490
|
+
GateDecisionObserver,
|
|
491
|
+
GateDecisionSource,
|
|
302
492
|
GateDeniedDecision,
|
|
303
493
|
GateDenyHandler,
|
|
304
494
|
GatePolicyResult,
|
|
305
495
|
GatePort,
|
|
496
|
+
PolicyBatch,
|
|
497
|
+
PolicyBatchBooleanMap,
|
|
498
|
+
PolicyBatchCheck,
|
|
499
|
+
PolicyBatchDecisionMap,
|
|
306
500
|
PolicyContextFromDefinitions,
|
|
307
501
|
PolicyDefinition,
|
|
308
502
|
PolicyMapFromDefinitions,
|
package/src/ports/policy.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|