@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @beignet/core
2
2
 
3
+ ## 0.0.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 73a682a: Make the type-preserving curried route-group form the canonical Beignet route
8
+ API across generated apps, docs, READMEs, and examples. `beignet doctor` now
9
+ warns when feature route groups use the direct generic form, and the direct
10
+ overload is deprecated because TypeScript erases per-route contract output
11
+ checks there.
12
+ - 2a02469: Add `createErrorReportingHooks(...)` for capturing unexpected HTTP failures
13
+ through `ctx.ports.errorReporter`, and wire generated apps to install the hook
14
+ with a no-op local reporter by default.
15
+ - 7d3c980: Add provider-neutral inbound webhook definitions, verifiers, HMAC verification, memory test verifier, and a generic Next.js webhook route adapter.
16
+
17
+ ## 0.0.14
18
+
19
+ ### Patch Changes
20
+
21
+ - 0b106c3: Add first-class error reporting primitives and a Sentry-backed error reporting
22
+ provider.
23
+ - 6828442: Add first-class feature flag primitives, an OpenFeature-backed flags provider,
24
+ and devtools support for flag evaluation, exposure, and tracking events.
25
+ - edf5ddc: Add first-class entitlement primitives for product access checks, devtools-ready entitlement decision observation, and doctor checks for entitlement wiring drift. Generated billing slices expose a billing-backed entitlements port.
26
+ - bcd3864: Add lease-backed lock primitives and a Redis-backed locks provider.
27
+ - d51a35f: Add the provider-neutral payments port, memory payments adapter, Stripe payments provider, Next.js payment webhook route helper, `beignet make payments`, and first-class payments devtools view.
28
+ - 655c101: Add keyed authorization batch checks and best-effort policy decision observation, plus a first-class devtools watcher/view for policy decision events.
29
+ - bcd3864: Add a provider-neutral search port, memory adapter, and Meilisearch provider.
30
+
3
31
  ## 0.0.13
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -8,8 +8,9 @@
8
8
 
9
9
  This package provides Beignet's framework primitives: contracts, server runtime,
10
10
  typed client, use cases, ports, domain helpers, app errors, config, events,
11
- idempotency, outbox, mail, notifications, schedules, uploads, pagination helpers,
12
- testing helpers, and OpenAPI generation.
11
+ idempotency, locks, outbox, mail, notifications, payments, search, webhooks,
12
+ feature flags, error reporting, schedules, uploads, entitlements, pagination
13
+ helpers, testing helpers, and OpenAPI generation.
13
14
 
14
15
  ## Installation
15
16
 
@@ -43,18 +44,24 @@ name the framework area they depend on.
43
44
  | `@beignet/core/config` | Environment config validation |
44
45
  | `@beignet/core/contracts` | HTTP contract builders, types, path helpers, and contract metadata |
45
46
  | `@beignet/core/domain` | Entities, value objects, and domain events |
47
+ | `@beignet/core/entitlements` | Product access decision types, helpers, and static entitlement adapter |
48
+ | `@beignet/core/error-reporting` | Error reporting port, memory adapter, no-op adapter, and helpers |
46
49
  | `@beignet/core/errors` | Error catalogs and response helpers |
47
50
  | `@beignet/core/events` | Events and listeners |
51
+ | `@beignet/core/flags` | Feature flag definitions, FlagsPort, memory/static adapters, and helpers |
48
52
  | `@beignet/core/idempotency` | Retry-safe command, webhook, and job primitives |
49
53
  | `@beignet/core/jobs` | Job definitions, retry policies, and inline job dispatch |
54
+ | `@beignet/core/locks` | Lease-backed LocksPort, memory adapter, memory provider, and helpers |
50
55
  | `@beignet/core/mail` | Mail port, memory mailer, and memory mailer provider |
51
56
  | `@beignet/core/notifications` | Notification definitions, dispatchers, inline notifications provider, mail channels, and test adapters |
52
57
  | `@beignet/core/openapi` | OpenAPI generation |
53
58
  | `@beignet/core/outbox` | Durable event and job outbox |
59
+ | `@beignet/core/payments` | Payments port, memory payments adapter, and memory payments provider |
54
60
  | `@beignet/core/pagination` | Offset/cursor page types, normalizers, and result helpers |
55
61
  | `@beignet/core/ports` | App-facing ports, auth, audit, policies, cache, storage, logging, and redaction |
56
62
  | `@beignet/core/ports/testing` | Port and policy test helpers |
57
63
  | `@beignet/core/providers` | Provider lifecycle and instrumentation primitives |
64
+ | `@beignet/core/search` | Search index definitions, SearchPort, memory adapter, memory provider, and helpers |
58
65
  | `@beignet/core/schedules` | Schedule primitives |
59
66
  | `@beignet/core/server` | Framework-agnostic server runtime and hook helpers |
60
67
  | `@beignet/core/server-only` | Static lint marker for modules that must stay out of client bundles |
@@ -63,6 +70,7 @@ name the framework area they depend on.
63
70
  | `@beignet/core/tracing` | Dependency-free W3C trace context primitives |
64
71
  | `@beignet/core/uploads` | Upload definitions, router, signer port, and test signer |
65
72
  | `@beignet/core/uploads/client` | Browser upload client for server and direct uploads |
73
+ | `@beignet/core/webhooks` | Inbound webhook definitions, verifiers, memory test verifier, and HMAC verifier |
66
74
 
67
75
  ## Durable failure language
68
76
 
@@ -177,6 +185,189 @@ Replace `createMemoryMailerProvider(...)` with a real mail provider such as
177
185
  keep the inline notifications provider and run it from jobs or listeners, or
178
186
  replace it with an app-owned dispatcher when delivery needs custom semantics.
179
187
 
188
+ ## Entitlements
189
+
190
+ Use `@beignet/core/entitlements` for product access decisions derived from
191
+ app-owned billing or plan state. The resolver maps durable app state to
192
+ allow/deny decisions; `requireEntitlement(...)` enforces the decision from a
193
+ use case and throws a framework-owned 403 by default.
194
+
195
+ ```typescript
196
+ import {
197
+ createEntitlements,
198
+ type EntitlementDecisionObserver,
199
+ requireEntitlement,
200
+ } from "@beignet/core/entitlements";
201
+
202
+ function createBillingEntitlements(
203
+ billing: BillingRepository,
204
+ recordDecision?: EntitlementDecisionObserver,
205
+ ) {
206
+ return createEntitlements({
207
+ async inspect(input) {
208
+ const account = await billing.findByTenantId(input.subject.id);
209
+ return account?.status === "active";
210
+ },
211
+ onDecision: recordDecision,
212
+ });
213
+ }
214
+
215
+ await requireEntitlement(ctx, {
216
+ entitlement: "todos.create",
217
+ subject: { type: "tenant", id: tenantId },
218
+ });
219
+ ```
220
+
221
+ `onDecision` is diagnostic only. Observer errors are ignored and cannot change
222
+ the entitlement result.
223
+
224
+ ## Feature flags
225
+
226
+ Use `@beignet/core/flags` for typed feature flag definitions and
227
+ provider-neutral evaluation. Flags always carry a default value, and provider
228
+ failures return that default instead of throwing into product workflows.
229
+
230
+ ```typescript
231
+ import { defineFlag, defineFlags } from "@beignet/core/flags";
232
+
233
+ export const billingFlags = defineFlags({
234
+ newCheckout: defineFlag.boolean("billing.new-checkout", {
235
+ default: false,
236
+ }),
237
+ });
238
+
239
+ const enabled = await ctx.ports.flags.evaluate(billingFlags.newCheckout, {
240
+ context: {
241
+ targetingKey: ctx.actor.id,
242
+ tenant: ctx.tenant,
243
+ requestId: ctx.requestId,
244
+ },
245
+ });
246
+ ```
247
+
248
+ Plain evaluation does not record exposure. Call `recordExposure(...)`
249
+ explicitly when a user actually sees or can be affected by the flagged
250
+ behavior. Use `createMemoryFlags(...)` or `createStaticFlags(...)` in tests, or
251
+ install `@beignet/provider-flags-openfeature` for production providers.
252
+ String and number flags widen to `string` and `number` by default; pass a
253
+ generic when an app wants a closed variant union.
254
+
255
+ ## Error reporting
256
+
257
+ Use `@beignet/core/error-reporting` for provider-neutral exception and message
258
+ capture. The port accepts severity, tags, user, contexts, extra metadata, and
259
+ request/trace correlation IDs.
260
+
261
+ ```typescript
262
+ import { createMemoryErrorReporter } from "@beignet/core/error-reporting";
263
+ import { createErrorReportingHooks } from "@beignet/core/server";
264
+ import type { AppContext } from "@/app-context";
265
+
266
+ await ctx.ports.errorReporter.captureException(error, {
267
+ level: "error",
268
+ requestId: ctx.requestId,
269
+ traceId: ctx.traceId,
270
+ tags: { feature: "billing" },
271
+ });
272
+
273
+ const errorReporter = createMemoryErrorReporter();
274
+
275
+ export const hooks = [createErrorReportingHooks<AppContext>()];
276
+ ```
277
+
278
+ Use `createMemoryErrorReporter(...)` in tests, `createNoopErrorReporter()` when
279
+ an app needs a bound port without capture, and
280
+ `createErrorReportingHooks(...)` in `server/index.ts` to capture unexpected HTTP
281
+ failures without changing response mapping. Install
282
+ `@beignet/provider-error-reporting-sentry` for production providers.
283
+
284
+ ## Locks and leases
285
+
286
+ Use `@beignet/core/locks` for provider-neutral lease-backed lock coordination.
287
+ Locks prevent overlapping schedules, singleton jobs, cache stampedes, and short
288
+ critical sections across multiple workers or servers.
289
+
290
+ ```typescript
291
+ import { createMemoryLocks } from "@beignet/core/locks";
292
+
293
+ await ctx.ports.locks.withLease(
294
+ "schedule:daily-report",
295
+ { ttlMs: 60_000, waitMs: 0 },
296
+ async ({ lease }) => {
297
+ await runDailyReport(ctx, { fencingToken: lease.fencingToken });
298
+ },
299
+ );
300
+
301
+ const locks = createMemoryLocks();
302
+ ```
303
+
304
+ Use `createMemoryLocks(...)` in tests, `createMemoryLocksProvider()` for local
305
+ provider wiring, or install `@beignet/provider-locks-redis` for production
306
+ leases.
307
+
308
+ ## Search
309
+
310
+ Use `@beignet/core/search` for provider-neutral search index definitions,
311
+ document indexing, and querying searchable read models.
312
+
313
+ ```typescript
314
+ import { defineSearchIndex } from "@beignet/core/search";
315
+
316
+ const issueSearchIndex = defineSearchIndex("issues", {
317
+ searchableAttributes: ["key", "title", "description"],
318
+ filterableAttributes: ["tenantId", "status"],
319
+ sortableAttributes: ["createdAt"],
320
+ });
321
+
322
+ await ctx.ports.search.indexDocuments(issueSearchIndex, issueDocument);
323
+
324
+ const results = await ctx.ports.search.search(issueSearchIndex, {
325
+ query: "billing",
326
+ filters: { tenantId },
327
+ sort: ["createdAt:desc"],
328
+ limit: 20,
329
+ });
330
+ ```
331
+
332
+ Use `createMemorySearch(...)` in tests, `createMemorySearchProvider()` for local
333
+ provider wiring, or install `@beignet/provider-search-meilisearch` for
334
+ production search.
335
+
336
+ ## Webhooks
337
+
338
+ Use `@beignet/core/webhooks` for provider-neutral inbound webhook definitions,
339
+ raw-body verification, typed event payload catalogs, and test verifiers.
340
+
341
+ ```typescript
342
+ import {
343
+ createHmacWebhookVerifier,
344
+ defineWebhook,
345
+ } from "@beignet/core/webhooks";
346
+ import { z } from "zod";
347
+
348
+ export const issueWebhook = defineWebhook("issues.provider", {
349
+ provider: "provider",
350
+ events: {
351
+ "issue.created": z.object({
352
+ id: z.string(),
353
+ type: z.literal("issue.created"),
354
+ issueId: z.string(),
355
+ }),
356
+ },
357
+ verifier: createHmacWebhookVerifier({
358
+ secret: process.env.PROVIDER_WEBHOOK_SECRET ?? "",
359
+ signatureHeader: "x-provider-signature",
360
+ signaturePrefix: "sha256=",
361
+ }),
362
+ });
363
+ ```
364
+
365
+ Use `createMemoryWebhookVerifier(...)` in tests and `createWebhookRoute(...)`
366
+ from `@beignet/next` to expose raw-body webhook routes in Next.js apps. Use a
367
+ provider package such as `@beignet/provider-webhooks-github` or
368
+ `@beignet/provider-webhooks-stripe` when a vendor has signature semantics beyond
369
+ the generic HMAC verifier.
370
+
180
371
  ## Provider metadata
181
372
 
182
373
  Reusable provider packages should declare static metadata in `package.json`
@@ -665,6 +856,11 @@ await tester.assertMatrix([
665
856
  },
666
857
  ]);
667
858
 
859
+ const permissions = await tester.gate.canMany(activity, {
860
+ publish: ["posts.publish", post],
861
+ });
862
+ expect(permissions.publish).toBe(true);
863
+
668
864
  assertAuditEntry(audit.entries, {
669
865
  action: "posts.publish",
670
866
  actorId: "user_1",
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @beignet/core/entitlements
3
+ *
4
+ * Provider-neutral product access primitives for Beignet applications.
5
+ */
6
+ type MaybePromise<T> = T | Promise<T>;
7
+ /**
8
+ * Stable app-defined capability key, such as "issues.create".
9
+ */
10
+ export type EntitlementKey = string;
11
+ /**
12
+ * Principal whose product access is being checked.
13
+ */
14
+ export type EntitlementSubject = {
15
+ /**
16
+ * App-defined subject type, commonly "tenant" or "account".
17
+ */
18
+ type: string;
19
+ /**
20
+ * Stable subject identifier.
21
+ */
22
+ id: string;
23
+ };
24
+ /**
25
+ * Input for checking whether a subject has access to a capability.
26
+ */
27
+ export type EntitlementCheckInput<TKey extends EntitlementKey = EntitlementKey> = {
28
+ /**
29
+ * Capability being checked.
30
+ */
31
+ entitlement: TKey;
32
+ /**
33
+ * Tenant, account, or other app-owned subject whose product access is checked.
34
+ */
35
+ subject: EntitlementSubject;
36
+ };
37
+ /**
38
+ * An entitlement decision that allows the capability.
39
+ */
40
+ export type EntitlementAllowedDecision = {
41
+ allowed: true;
42
+ };
43
+ /**
44
+ * An entitlement decision that denies the capability.
45
+ */
46
+ export type EntitlementDeniedDecision = {
47
+ allowed: false;
48
+ reason?: string;
49
+ code?: string;
50
+ details?: unknown;
51
+ };
52
+ /**
53
+ * Normalized entitlement decision.
54
+ */
55
+ export type EntitlementDecision = EntitlementAllowedDecision | EntitlementDeniedDecision;
56
+ /**
57
+ * Value an entitlement resolver may return.
58
+ */
59
+ export type EntitlementResolverResult = boolean | EntitlementDecision;
60
+ /**
61
+ * Entitlement port method that produced a decision.
62
+ */
63
+ export type EntitlementDecisionSource = "can" | "inspect" | "require";
64
+ /**
65
+ * Optional context for entitlement checks.
66
+ */
67
+ export type EntitlementCheckOptions<TContext = unknown> = {
68
+ /**
69
+ * Application context used for the check. Observers can use this to copy
70
+ * request correlation fields without coupling the resolver to app context.
71
+ */
72
+ ctx?: TContext;
73
+ /**
74
+ * Source to report to observers. Port methods set this automatically.
75
+ */
76
+ source?: EntitlementDecisionSource;
77
+ };
78
+ /**
79
+ * App-facing product access port.
80
+ */
81
+ export type EntitlementsPort<TKey extends EntitlementKey = EntitlementKey, TContext = unknown> = {
82
+ /**
83
+ * Return only whether the capability is allowed.
84
+ */
85
+ can(input: EntitlementCheckInput<TKey>, options?: EntitlementCheckOptions<TContext>): Promise<boolean>;
86
+ /**
87
+ * Return the full allow/deny decision without throwing.
88
+ */
89
+ inspect(input: EntitlementCheckInput<TKey>, options?: EntitlementCheckOptions<TContext>): Promise<EntitlementDecision>;
90
+ };
91
+ /**
92
+ * Function that decides whether an entitlement is granted.
93
+ */
94
+ export type EntitlementResolver<TKey extends EntitlementKey = EntitlementKey> = (input: EntitlementCheckInput<TKey>) => MaybePromise<EntitlementResolverResult>;
95
+ /**
96
+ * Best-effort entitlement decision observation emitted by an entitlements port.
97
+ *
98
+ * Observers are diagnostic only. They do not participate in entitlement
99
+ * control flow and thrown/rejected observer errors are ignored.
100
+ */
101
+ export type EntitlementDecisionObservation<TContext = unknown, TKey extends EntitlementKey = EntitlementKey> = {
102
+ /**
103
+ * Port helper that produced the decision.
104
+ */
105
+ source: EntitlementDecisionSource;
106
+ /**
107
+ * Application context passed by the caller, when available.
108
+ */
109
+ ctx?: TContext;
110
+ /**
111
+ * Capability being evaluated.
112
+ */
113
+ entitlement: TKey;
114
+ /**
115
+ * Subject whose product access is checked.
116
+ */
117
+ subject: EntitlementSubject;
118
+ /**
119
+ * Normalized decision, when resolver evaluation returned normally.
120
+ */
121
+ decision?: EntitlementDecision;
122
+ /**
123
+ * Error thrown by the resolver, when evaluation failed.
124
+ */
125
+ error?: unknown;
126
+ /**
127
+ * Resolver duration, excluding observer work.
128
+ */
129
+ durationMs: number;
130
+ /**
131
+ * Correlation fields copied from the context when present.
132
+ */
133
+ requestId?: string;
134
+ traceId?: string;
135
+ spanId?: string;
136
+ parentSpanId?: string;
137
+ traceparent?: string;
138
+ };
139
+ /**
140
+ * Best-effort observer called after each entitlement decision or resolver
141
+ * error.
142
+ */
143
+ export type EntitlementDecisionObserver<TContext = unknown, TKey extends EntitlementKey = EntitlementKey> = (observation: EntitlementDecisionObservation<TContext, TKey>) => MaybePromise<void>;
144
+ /**
145
+ * Options for `createEntitlements(...)`.
146
+ */
147
+ export type CreateEntitlementsOptions<TKey extends EntitlementKey = EntitlementKey, TContext = unknown> = {
148
+ /**
149
+ * Resolver that maps app-owned product state to an entitlement decision.
150
+ */
151
+ inspect: EntitlementResolver<TKey>;
152
+ /**
153
+ * Optional best-effort observer for entitlement decisions.
154
+ *
155
+ * This hook is diagnostic only: it cannot change decisions or thrown errors.
156
+ */
157
+ onDecision?: EntitlementDecisionObserver<TContext, TKey>;
158
+ };
159
+ /**
160
+ * Static grant map keyed by subject key.
161
+ */
162
+ export type StaticEntitlementGrantMap<TKey extends EntitlementKey = EntitlementKey> = Record<string, readonly TKey[]>;
163
+ /**
164
+ * Options for `createStaticEntitlements(...)`.
165
+ */
166
+ export type CreateStaticEntitlementsOptions<TKey extends EntitlementKey = EntitlementKey, TContext = unknown> = {
167
+ /**
168
+ * Granted capabilities keyed by subject. The default key is
169
+ * `${subject.type}:${subject.id}`.
170
+ */
171
+ grants: StaticEntitlementGrantMap<TKey>;
172
+ /**
173
+ * Optional subject key mapper.
174
+ */
175
+ subjectKey?: (subject: EntitlementSubject) => string;
176
+ /**
177
+ * Default denial decision fields.
178
+ */
179
+ denied?: Omit<EntitlementDeniedDecision, "allowed">;
180
+ /**
181
+ * Optional best-effort observer for entitlement decisions.
182
+ */
183
+ onDecision?: EntitlementDecisionObserver<TContext, TKey>;
184
+ };
185
+ /**
186
+ * Options accepted by `requireEntitlement(...)`.
187
+ */
188
+ export type RequireEntitlementOptions<TKey extends EntitlementKey = EntitlementKey> = {
189
+ /**
190
+ * Create the error to throw instead of the framework default.
191
+ */
192
+ error?: (decision: EntitlementDeniedDecision, input: EntitlementCheckInput<TKey>) => unknown;
193
+ };
194
+ /**
195
+ * Context shape consumed by `requireEntitlement(...)`.
196
+ */
197
+ export type EntitlementsContext<TKey extends EntitlementKey = EntitlementKey> = {
198
+ ports: {
199
+ entitlements: EntitlementsPort<TKey>;
200
+ };
201
+ };
202
+ /**
203
+ * Error thrown by `requireEntitlement(...)` when product access is denied.
204
+ */
205
+ export declare class EntitlementRequiredError extends Error {
206
+ readonly code: string;
207
+ readonly status = 403;
208
+ readonly details?: unknown;
209
+ readonly entitlement: string;
210
+ readonly subject: EntitlementSubject;
211
+ constructor(input: EntitlementCheckInput, decision?: EntitlementDeniedDecision);
212
+ }
213
+ /**
214
+ * Create an explicit allow decision.
215
+ */
216
+ export declare function allowEntitlement(): EntitlementAllowedDecision;
217
+ /**
218
+ * Create an explicit deny decision.
219
+ */
220
+ export declare function denyEntitlement(reasonOrDecision?: string | Omit<EntitlementDeniedDecision, "allowed">): EntitlementDeniedDecision;
221
+ /**
222
+ * Create an entitlement port from an app-owned resolver.
223
+ */
224
+ export declare function createEntitlements<TKey extends EntitlementKey = EntitlementKey, TContext = unknown>(options: CreateEntitlementsOptions<TKey, TContext>): EntitlementsPort<TKey, TContext>;
225
+ /**
226
+ * Create a static entitlement port for tests, starters, and simple apps.
227
+ */
228
+ export declare function createStaticEntitlements<TKey extends EntitlementKey = EntitlementKey, TContext = unknown>(options: CreateStaticEntitlementsOptions<TKey, TContext>): EntitlementsPort<TKey, TContext>;
229
+ /**
230
+ * Require an entitlement or throw a framework-owned 403 error.
231
+ */
232
+ export declare function requireEntitlement<TKey extends EntitlementKey = EntitlementKey>(ctx: EntitlementsContext<TKey>, input: EntitlementCheckInput<TKey>, options?: RequireEntitlementOptions<TKey>): Promise<EntitlementAllowedDecision>;
233
+ export {};
234
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entitlements/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @beignet/core/entitlements
3
+ *
4
+ * Provider-neutral product access primitives for Beignet applications.
5
+ */
6
+ /**
7
+ * Error thrown by `requireEntitlement(...)` when product access is denied.
8
+ */
9
+ export class EntitlementRequiredError extends Error {
10
+ code;
11
+ status = 403;
12
+ details;
13
+ entitlement;
14
+ subject;
15
+ constructor(input, decision = denyEntitlement()) {
16
+ super(decision.reason ??
17
+ `Entitlement "${input.entitlement}" is required for this action.`);
18
+ this.name = "EntitlementRequiredError";
19
+ this.code = decision.code ?? "ENTITLEMENT_REQUIRED";
20
+ this.details = decision.details;
21
+ this.entitlement = input.entitlement;
22
+ this.subject = input.subject;
23
+ }
24
+ }
25
+ /**
26
+ * Create an explicit allow decision.
27
+ */
28
+ export function allowEntitlement() {
29
+ return { allowed: true };
30
+ }
31
+ /**
32
+ * Create an explicit deny decision.
33
+ */
34
+ export function denyEntitlement(reasonOrDecision) {
35
+ if (typeof reasonOrDecision === "string") {
36
+ return { allowed: false, reason: reasonOrDecision };
37
+ }
38
+ return {
39
+ allowed: false,
40
+ ...reasonOrDecision,
41
+ };
42
+ }
43
+ /**
44
+ * Create an entitlement port from an app-owned resolver.
45
+ */
46
+ export function createEntitlements(options) {
47
+ const observeDecision = createDecisionObserver(options.onDecision);
48
+ async function evaluate(input, checkOptions) {
49
+ const startedAt = entitlementNow();
50
+ try {
51
+ const decision = normalizeEntitlementDecision(await options.inspect(input));
52
+ observeDecision({
53
+ source: checkOptions?.source ?? "inspect",
54
+ ctx: checkOptions?.ctx,
55
+ entitlement: input.entitlement,
56
+ subject: input.subject,
57
+ decision,
58
+ durationMs: entitlementNow() - startedAt,
59
+ });
60
+ return decision;
61
+ }
62
+ catch (error) {
63
+ observeDecision({
64
+ source: checkOptions?.source ?? "inspect",
65
+ ctx: checkOptions?.ctx,
66
+ entitlement: input.entitlement,
67
+ subject: input.subject,
68
+ error,
69
+ durationMs: entitlementNow() - startedAt,
70
+ });
71
+ throw error;
72
+ }
73
+ }
74
+ return {
75
+ async can(input, checkOptions) {
76
+ return (await evaluate(input, { ...checkOptions, source: "can" }))
77
+ .allowed;
78
+ },
79
+ inspect(input, checkOptions) {
80
+ return evaluate(input, {
81
+ ...checkOptions,
82
+ source: checkOptions?.source ?? "inspect",
83
+ });
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Create a static entitlement port for tests, starters, and simple apps.
89
+ */
90
+ export function createStaticEntitlements(options) {
91
+ const subjectKey = options.subjectKey ?? defaultSubjectKey;
92
+ return createEntitlements({
93
+ inspect(input) {
94
+ const granted = options.grants[subjectKey(input.subject)] ?? [];
95
+ if (granted.includes(input.entitlement))
96
+ return allowEntitlement();
97
+ return denyEntitlement({
98
+ reason: `Entitlement "${input.entitlement}" is not granted.`,
99
+ code: "ENTITLEMENT_NOT_GRANTED",
100
+ details: {
101
+ entitlement: input.entitlement,
102
+ subject: input.subject,
103
+ },
104
+ ...options.denied,
105
+ });
106
+ },
107
+ onDecision: options.onDecision,
108
+ });
109
+ }
110
+ /**
111
+ * Require an entitlement or throw a framework-owned 403 error.
112
+ */
113
+ export async function requireEntitlement(ctx, input, options) {
114
+ const decision = await ctx.ports.entitlements.inspect(input, {
115
+ ctx,
116
+ source: "require",
117
+ });
118
+ if (decision.allowed)
119
+ return decision;
120
+ throw options?.error
121
+ ? options.error(decision, input)
122
+ : new EntitlementRequiredError(input, decision);
123
+ }
124
+ function normalizeEntitlementDecision(result) {
125
+ if (typeof result === "boolean") {
126
+ return result ? allowEntitlement() : denyEntitlement();
127
+ }
128
+ return result;
129
+ }
130
+ function entitlementNow() {
131
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
132
+ }
133
+ function contextStringField(ctx, field) {
134
+ if (!ctx || typeof ctx !== "object")
135
+ return undefined;
136
+ const value = ctx[field];
137
+ return typeof value === "string" ? value : undefined;
138
+ }
139
+ function enrichObservation(observation) {
140
+ return {
141
+ ...observation,
142
+ requestId: contextStringField(observation.ctx, "requestId"),
143
+ traceId: contextStringField(observation.ctx, "traceId"),
144
+ spanId: contextStringField(observation.ctx, "spanId"),
145
+ parentSpanId: contextStringField(observation.ctx, "parentSpanId"),
146
+ traceparent: contextStringField(observation.ctx, "traceparent"),
147
+ };
148
+ }
149
+ function isPromiseLike(value) {
150
+ return (value !== null &&
151
+ (typeof value === "object" || typeof value === "function") &&
152
+ typeof value.then === "function");
153
+ }
154
+ function createDecisionObserver(observer) {
155
+ return (observation) => {
156
+ if (!observer)
157
+ return;
158
+ try {
159
+ const result = observer(enrichObservation(observation));
160
+ if (isPromiseLike(result)) {
161
+ Promise.resolve(result).catch(() => undefined);
162
+ }
163
+ }
164
+ catch {
165
+ // Observers are diagnostic only and must not affect entitlement checks.
166
+ }
167
+ };
168
+ }
169
+ function defaultSubjectKey(subject) {
170
+ return `${subject.type}:${subject.id}`;
171
+ }
172
+ //# sourceMappingURL=index.js.map