@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,479 @@
1
+ /**
2
+ * @beignet/core/entitlements
3
+ *
4
+ * Provider-neutral product access primitives for Beignet applications.
5
+ */
6
+
7
+ type MaybePromise<T> = T | Promise<T>;
8
+
9
+ /**
10
+ * Stable app-defined capability key, such as "issues.create".
11
+ */
12
+ export type EntitlementKey = string;
13
+
14
+ /**
15
+ * Principal whose product access is being checked.
16
+ */
17
+ export type EntitlementSubject = {
18
+ /**
19
+ * App-defined subject type, commonly "tenant" or "account".
20
+ */
21
+ type: string;
22
+ /**
23
+ * Stable subject identifier.
24
+ */
25
+ id: string;
26
+ };
27
+
28
+ /**
29
+ * Input for checking whether a subject has access to a capability.
30
+ */
31
+ export type EntitlementCheckInput<
32
+ TKey extends EntitlementKey = EntitlementKey,
33
+ > = {
34
+ /**
35
+ * Capability being checked.
36
+ */
37
+ entitlement: TKey;
38
+ /**
39
+ * Tenant, account, or other app-owned subject whose product access is checked.
40
+ */
41
+ subject: EntitlementSubject;
42
+ };
43
+
44
+ /**
45
+ * An entitlement decision that allows the capability.
46
+ */
47
+ export type EntitlementAllowedDecision = {
48
+ allowed: true;
49
+ };
50
+
51
+ /**
52
+ * An entitlement decision that denies the capability.
53
+ */
54
+ export type EntitlementDeniedDecision = {
55
+ allowed: false;
56
+ reason?: string;
57
+ code?: string;
58
+ details?: unknown;
59
+ };
60
+
61
+ /**
62
+ * Normalized entitlement decision.
63
+ */
64
+ export type EntitlementDecision =
65
+ | EntitlementAllowedDecision
66
+ | EntitlementDeniedDecision;
67
+
68
+ /**
69
+ * Value an entitlement resolver may return.
70
+ */
71
+ export type EntitlementResolverResult = boolean | EntitlementDecision;
72
+
73
+ /**
74
+ * Entitlement port method that produced a decision.
75
+ */
76
+ export type EntitlementDecisionSource = "can" | "inspect" | "require";
77
+
78
+ /**
79
+ * Optional context for entitlement checks.
80
+ */
81
+ export type EntitlementCheckOptions<TContext = unknown> = {
82
+ /**
83
+ * Application context used for the check. Observers can use this to copy
84
+ * request correlation fields without coupling the resolver to app context.
85
+ */
86
+ ctx?: TContext;
87
+ /**
88
+ * Source to report to observers. Port methods set this automatically.
89
+ */
90
+ source?: EntitlementDecisionSource;
91
+ };
92
+
93
+ /**
94
+ * App-facing product access port.
95
+ */
96
+ export type EntitlementsPort<
97
+ TKey extends EntitlementKey = EntitlementKey,
98
+ TContext = unknown,
99
+ > = {
100
+ /**
101
+ * Return only whether the capability is allowed.
102
+ */
103
+ can(
104
+ input: EntitlementCheckInput<TKey>,
105
+ options?: EntitlementCheckOptions<TContext>,
106
+ ): Promise<boolean>;
107
+ /**
108
+ * Return the full allow/deny decision without throwing.
109
+ */
110
+ inspect(
111
+ input: EntitlementCheckInput<TKey>,
112
+ options?: EntitlementCheckOptions<TContext>,
113
+ ): Promise<EntitlementDecision>;
114
+ };
115
+
116
+ /**
117
+ * Function that decides whether an entitlement is granted.
118
+ */
119
+ export type EntitlementResolver<TKey extends EntitlementKey = EntitlementKey> =
120
+ (
121
+ input: EntitlementCheckInput<TKey>,
122
+ ) => MaybePromise<EntitlementResolverResult>;
123
+
124
+ /**
125
+ * Best-effort entitlement decision observation emitted by an entitlements port.
126
+ *
127
+ * Observers are diagnostic only. They do not participate in entitlement
128
+ * control flow and thrown/rejected observer errors are ignored.
129
+ */
130
+ export type EntitlementDecisionObservation<
131
+ TContext = unknown,
132
+ TKey extends EntitlementKey = EntitlementKey,
133
+ > = {
134
+ /**
135
+ * Port helper that produced the decision.
136
+ */
137
+ source: EntitlementDecisionSource;
138
+ /**
139
+ * Application context passed by the caller, when available.
140
+ */
141
+ ctx?: TContext;
142
+ /**
143
+ * Capability being evaluated.
144
+ */
145
+ entitlement: TKey;
146
+ /**
147
+ * Subject whose product access is checked.
148
+ */
149
+ subject: EntitlementSubject;
150
+ /**
151
+ * Normalized decision, when resolver evaluation returned normally.
152
+ */
153
+ decision?: EntitlementDecision;
154
+ /**
155
+ * Error thrown by the resolver, when evaluation failed.
156
+ */
157
+ error?: unknown;
158
+ /**
159
+ * Resolver duration, excluding observer work.
160
+ */
161
+ durationMs: number;
162
+ /**
163
+ * Correlation fields copied from the context when present.
164
+ */
165
+ requestId?: string;
166
+ traceId?: string;
167
+ spanId?: string;
168
+ parentSpanId?: string;
169
+ traceparent?: string;
170
+ };
171
+
172
+ /**
173
+ * Best-effort observer called after each entitlement decision or resolver
174
+ * error.
175
+ */
176
+ export type EntitlementDecisionObserver<
177
+ TContext = unknown,
178
+ TKey extends EntitlementKey = EntitlementKey,
179
+ > = (
180
+ observation: EntitlementDecisionObservation<TContext, TKey>,
181
+ ) => MaybePromise<void>;
182
+
183
+ /**
184
+ * Options for `createEntitlements(...)`.
185
+ */
186
+ export type CreateEntitlementsOptions<
187
+ TKey extends EntitlementKey = EntitlementKey,
188
+ TContext = unknown,
189
+ > = {
190
+ /**
191
+ * Resolver that maps app-owned product state to an entitlement decision.
192
+ */
193
+ inspect: EntitlementResolver<TKey>;
194
+ /**
195
+ * Optional best-effort observer for entitlement decisions.
196
+ *
197
+ * This hook is diagnostic only: it cannot change decisions or thrown errors.
198
+ */
199
+ onDecision?: EntitlementDecisionObserver<TContext, TKey>;
200
+ };
201
+
202
+ /**
203
+ * Static grant map keyed by subject key.
204
+ */
205
+ export type StaticEntitlementGrantMap<
206
+ TKey extends EntitlementKey = EntitlementKey,
207
+ > = Record<string, readonly TKey[]>;
208
+
209
+ /**
210
+ * Options for `createStaticEntitlements(...)`.
211
+ */
212
+ export type CreateStaticEntitlementsOptions<
213
+ TKey extends EntitlementKey = EntitlementKey,
214
+ TContext = unknown,
215
+ > = {
216
+ /**
217
+ * Granted capabilities keyed by subject. The default key is
218
+ * `${subject.type}:${subject.id}`.
219
+ */
220
+ grants: StaticEntitlementGrantMap<TKey>;
221
+ /**
222
+ * Optional subject key mapper.
223
+ */
224
+ subjectKey?: (subject: EntitlementSubject) => string;
225
+ /**
226
+ * Default denial decision fields.
227
+ */
228
+ denied?: Omit<EntitlementDeniedDecision, "allowed">;
229
+ /**
230
+ * Optional best-effort observer for entitlement decisions.
231
+ */
232
+ onDecision?: EntitlementDecisionObserver<TContext, TKey>;
233
+ };
234
+
235
+ /**
236
+ * Options accepted by `requireEntitlement(...)`.
237
+ */
238
+ export type RequireEntitlementOptions<
239
+ TKey extends EntitlementKey = EntitlementKey,
240
+ > = {
241
+ /**
242
+ * Create the error to throw instead of the framework default.
243
+ */
244
+ error?: (
245
+ decision: EntitlementDeniedDecision,
246
+ input: EntitlementCheckInput<TKey>,
247
+ ) => unknown;
248
+ };
249
+
250
+ /**
251
+ * Context shape consumed by `requireEntitlement(...)`.
252
+ */
253
+ export type EntitlementsContext<TKey extends EntitlementKey = EntitlementKey> =
254
+ {
255
+ ports: {
256
+ entitlements: EntitlementsPort<TKey>;
257
+ };
258
+ };
259
+
260
+ /**
261
+ * Error thrown by `requireEntitlement(...)` when product access is denied.
262
+ */
263
+ export class EntitlementRequiredError extends Error {
264
+ readonly code: string;
265
+ readonly status = 403;
266
+ readonly details?: unknown;
267
+ readonly entitlement: string;
268
+ readonly subject: EntitlementSubject;
269
+
270
+ constructor(
271
+ input: EntitlementCheckInput,
272
+ decision: EntitlementDeniedDecision = denyEntitlement(),
273
+ ) {
274
+ super(
275
+ decision.reason ??
276
+ `Entitlement "${input.entitlement}" is required for this action.`,
277
+ );
278
+ this.name = "EntitlementRequiredError";
279
+ this.code = decision.code ?? "ENTITLEMENT_REQUIRED";
280
+ this.details = decision.details;
281
+ this.entitlement = input.entitlement;
282
+ this.subject = input.subject;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Create an explicit allow decision.
288
+ */
289
+ export function allowEntitlement(): EntitlementAllowedDecision {
290
+ return { allowed: true };
291
+ }
292
+
293
+ /**
294
+ * Create an explicit deny decision.
295
+ */
296
+ export function denyEntitlement(
297
+ reasonOrDecision?: string | Omit<EntitlementDeniedDecision, "allowed">,
298
+ ): EntitlementDeniedDecision {
299
+ if (typeof reasonOrDecision === "string") {
300
+ return { allowed: false, reason: reasonOrDecision };
301
+ }
302
+
303
+ return {
304
+ allowed: false,
305
+ ...reasonOrDecision,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Create an entitlement port from an app-owned resolver.
311
+ */
312
+ export function createEntitlements<
313
+ TKey extends EntitlementKey = EntitlementKey,
314
+ TContext = unknown,
315
+ >(
316
+ options: CreateEntitlementsOptions<TKey, TContext>,
317
+ ): EntitlementsPort<TKey, TContext> {
318
+ const observeDecision = createDecisionObserver(options.onDecision);
319
+
320
+ async function evaluate(
321
+ input: EntitlementCheckInput<TKey>,
322
+ checkOptions: EntitlementCheckOptions<TContext> | undefined,
323
+ ): Promise<EntitlementDecision> {
324
+ const startedAt = entitlementNow();
325
+
326
+ try {
327
+ const decision = normalizeEntitlementDecision(
328
+ await options.inspect(input),
329
+ );
330
+ observeDecision({
331
+ source: checkOptions?.source ?? "inspect",
332
+ ctx: checkOptions?.ctx,
333
+ entitlement: input.entitlement,
334
+ subject: input.subject,
335
+ decision,
336
+ durationMs: entitlementNow() - startedAt,
337
+ });
338
+
339
+ return decision;
340
+ } catch (error) {
341
+ observeDecision({
342
+ source: checkOptions?.source ?? "inspect",
343
+ ctx: checkOptions?.ctx,
344
+ entitlement: input.entitlement,
345
+ subject: input.subject,
346
+ error,
347
+ durationMs: entitlementNow() - startedAt,
348
+ });
349
+
350
+ throw error;
351
+ }
352
+ }
353
+
354
+ return {
355
+ async can(input, checkOptions) {
356
+ return (await evaluate(input, { ...checkOptions, source: "can" }))
357
+ .allowed;
358
+ },
359
+ inspect(input, checkOptions) {
360
+ return evaluate(input, {
361
+ ...checkOptions,
362
+ source: checkOptions?.source ?? "inspect",
363
+ });
364
+ },
365
+ };
366
+ }
367
+
368
+ /**
369
+ * Create a static entitlement port for tests, starters, and simple apps.
370
+ */
371
+ export function createStaticEntitlements<
372
+ TKey extends EntitlementKey = EntitlementKey,
373
+ TContext = unknown,
374
+ >(
375
+ options: CreateStaticEntitlementsOptions<TKey, TContext>,
376
+ ): EntitlementsPort<TKey, TContext> {
377
+ const subjectKey = options.subjectKey ?? defaultSubjectKey;
378
+
379
+ return createEntitlements<TKey, TContext>({
380
+ inspect(input) {
381
+ const granted = options.grants[subjectKey(input.subject)] ?? [];
382
+ if (granted.includes(input.entitlement)) return allowEntitlement();
383
+
384
+ return denyEntitlement({
385
+ reason: `Entitlement "${input.entitlement}" is not granted.`,
386
+ code: "ENTITLEMENT_NOT_GRANTED",
387
+ details: {
388
+ entitlement: input.entitlement,
389
+ subject: input.subject,
390
+ },
391
+ ...options.denied,
392
+ });
393
+ },
394
+ onDecision: options.onDecision,
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Require an entitlement or throw a framework-owned 403 error.
400
+ */
401
+ export async function requireEntitlement<
402
+ TKey extends EntitlementKey = EntitlementKey,
403
+ >(
404
+ ctx: EntitlementsContext<TKey>,
405
+ input: EntitlementCheckInput<TKey>,
406
+ options?: RequireEntitlementOptions<TKey>,
407
+ ): Promise<EntitlementAllowedDecision> {
408
+ const decision = await ctx.ports.entitlements.inspect(input, {
409
+ ctx,
410
+ source: "require",
411
+ });
412
+ if (decision.allowed) return decision;
413
+
414
+ throw options?.error
415
+ ? options.error(decision, input)
416
+ : new EntitlementRequiredError(input, decision);
417
+ }
418
+
419
+ function normalizeEntitlementDecision(
420
+ result: EntitlementResolverResult,
421
+ ): EntitlementDecision {
422
+ if (typeof result === "boolean") {
423
+ return result ? allowEntitlement() : denyEntitlement();
424
+ }
425
+
426
+ return result;
427
+ }
428
+
429
+ function entitlementNow(): number {
430
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
431
+ }
432
+
433
+ function contextStringField(ctx: unknown, field: string): string | undefined {
434
+ if (!ctx || typeof ctx !== "object") return undefined;
435
+ const value = (ctx as Record<string, unknown>)[field];
436
+ return typeof value === "string" ? value : undefined;
437
+ }
438
+
439
+ function enrichObservation<TContext, TKey extends EntitlementKey>(
440
+ observation: EntitlementDecisionObservation<TContext, TKey>,
441
+ ): EntitlementDecisionObservation<TContext, TKey> {
442
+ return {
443
+ ...observation,
444
+ requestId: contextStringField(observation.ctx, "requestId"),
445
+ traceId: contextStringField(observation.ctx, "traceId"),
446
+ spanId: contextStringField(observation.ctx, "spanId"),
447
+ parentSpanId: contextStringField(observation.ctx, "parentSpanId"),
448
+ traceparent: contextStringField(observation.ctx, "traceparent"),
449
+ };
450
+ }
451
+
452
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
453
+ return (
454
+ value !== null &&
455
+ (typeof value === "object" || typeof value === "function") &&
456
+ typeof (value as { then?: unknown }).then === "function"
457
+ );
458
+ }
459
+
460
+ function createDecisionObserver<TContext, TKey extends EntitlementKey>(
461
+ observer: EntitlementDecisionObserver<TContext, TKey> | undefined,
462
+ ): (observation: EntitlementDecisionObservation<TContext, TKey>) => void {
463
+ return (observation) => {
464
+ if (!observer) return;
465
+
466
+ try {
467
+ const result = observer(enrichObservation(observation));
468
+ if (isPromiseLike(result)) {
469
+ Promise.resolve(result).catch(() => undefined);
470
+ }
471
+ } catch {
472
+ // Observers are diagnostic only and must not affect entitlement checks.
473
+ }
474
+ };
475
+ }
476
+
477
+ function defaultSubjectKey(subject: EntitlementSubject): string {
478
+ return `${subject.type}:${subject.id}`;
479
+ }