@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +159 -9
  3. package/dist/client/client.d.ts.map +1 -1
  4. package/dist/client/client.js +29 -1
  5. package/dist/client/client.js.map +1 -1
  6. package/dist/entitlements/index.d.ts +234 -0
  7. package/dist/entitlements/index.d.ts.map +1 -0
  8. package/dist/entitlements/index.js +172 -0
  9. package/dist/entitlements/index.js.map +1 -0
  10. package/dist/error-reporting/index.d.ts +131 -0
  11. package/dist/error-reporting/index.d.ts.map +1 -0
  12. package/dist/error-reporting/index.js +112 -0
  13. package/dist/error-reporting/index.js.map +1 -0
  14. package/dist/flags/index.d.ts +293 -0
  15. package/dist/flags/index.d.ts.map +1 -0
  16. package/dist/flags/index.js +204 -0
  17. package/dist/flags/index.js.map +1 -0
  18. package/dist/locks/index.d.ts +155 -0
  19. package/dist/locks/index.d.ts.map +1 -0
  20. package/dist/locks/index.js +248 -0
  21. package/dist/locks/index.js.map +1 -0
  22. package/dist/payments/index.d.ts +430 -0
  23. package/dist/payments/index.d.ts.map +1 -0
  24. package/dist/payments/index.js +230 -0
  25. package/dist/payments/index.js.map +1 -0
  26. package/dist/ports/index.d.ts +61 -1
  27. package/dist/ports/index.d.ts.map +1 -1
  28. package/dist/ports/index.js +33 -0
  29. package/dist/ports/index.js.map +1 -1
  30. package/dist/ports/policy.d.ts +104 -0
  31. package/dist/ports/policy.d.ts.map +1 -1
  32. package/dist/ports/policy.js +102 -7
  33. package/dist/ports/policy.js.map +1 -1
  34. package/dist/search/index.d.ts +175 -0
  35. package/dist/search/index.d.ts.map +1 -0
  36. package/dist/search/index.js +344 -0
  37. package/dist/search/index.js.map +1 -0
  38. package/dist/server/server.d.ts.map +1 -1
  39. package/dist/server/server.js +9 -1
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/testing/index.d.ts +46 -1
  42. package/dist/testing/index.d.ts.map +1 -1
  43. package/dist/testing/index.js +33 -1
  44. package/dist/testing/index.js.map +1 -1
  45. package/package.json +25 -1
  46. package/src/client/client.ts +33 -1
  47. package/src/entitlements/index.ts +479 -0
  48. package/src/error-reporting/index.ts +273 -0
  49. package/src/flags/index.ts +608 -0
  50. package/src/locks/index.ts +440 -0
  51. package/src/payments/index.ts +699 -0
  52. package/src/ports/index.ts +194 -0
  53. package/src/ports/policy.ts +272 -7
  54. package/src/search/index.ts +632 -0
  55. package/src/server/server.ts +15 -0
  56. package/src/testing/index.ts +83 -1
@@ -0,0 +1,608 @@
1
+ /**
2
+ * @beignet/core/flags
3
+ *
4
+ * Provider-neutral feature flag primitives for Beignet applications.
5
+ */
6
+
7
+ type MaybePromise<T> = T | Promise<T>;
8
+
9
+ /**
10
+ * JSON-compatible value used by object flags and flag metadata.
11
+ */
12
+ export type FlagJsonValue =
13
+ | null
14
+ | boolean
15
+ | number
16
+ | string
17
+ | readonly FlagJsonValue[]
18
+ | { readonly [key: string]: FlagJsonValue };
19
+
20
+ /**
21
+ * Object-valued flags intentionally exclude scalar JSON values so the flag
22
+ * kind remains predictable.
23
+ */
24
+ export type FlagObjectValue =
25
+ | readonly FlagJsonValue[]
26
+ | { readonly [key: string]: FlagJsonValue };
27
+
28
+ /**
29
+ * Value kinds supported by Beignet flags.
30
+ */
31
+ export type FlagValue = boolean | string | number | FlagObjectValue;
32
+
33
+ /**
34
+ * Stable feature flag key.
35
+ */
36
+ export type FlagKey = string;
37
+
38
+ /**
39
+ * Feature flag value kind.
40
+ */
41
+ export type FlagKind = "boolean" | "string" | "number" | "object";
42
+
43
+ /**
44
+ * Subject used for provider-neutral flag targeting.
45
+ */
46
+ export type FlagSubject = {
47
+ type: string;
48
+ id: string;
49
+ };
50
+
51
+ /**
52
+ * Tenant or account used for provider-neutral flag targeting.
53
+ */
54
+ export type FlagTenant = {
55
+ id: string;
56
+ slug?: string;
57
+ };
58
+
59
+ /**
60
+ * Attribute value accepted by the provider-neutral flag context.
61
+ */
62
+ export type FlagAttributeValue =
63
+ | null
64
+ | boolean
65
+ | number
66
+ | string
67
+ | readonly FlagAttributeValue[]
68
+ | { readonly [key: string]: FlagAttributeValue };
69
+
70
+ /**
71
+ * Context supplied to flag evaluations, exposure recording, and tracking.
72
+ */
73
+ export type FlagEvaluationContext = {
74
+ /**
75
+ * Stable key used by providers for rollout bucketing and targeting.
76
+ */
77
+ targetingKey: string;
78
+ /**
79
+ * Actor, user, service, tenant, or account being targeted.
80
+ */
81
+ subject?: FlagSubject;
82
+ /**
83
+ * Tenant/account scope, when available.
84
+ */
85
+ tenant?: FlagTenant;
86
+ /**
87
+ * Provider-neutral targeting attributes. Keep private data out unless the
88
+ * selected provider and retention policy are approved for it.
89
+ */
90
+ attributes?: Record<string, FlagAttributeValue>;
91
+ /**
92
+ * Attribute keys that should be treated as private by providers and
93
+ * instrumentation.
94
+ */
95
+ privateAttributes?: readonly string[];
96
+ requestId?: string;
97
+ traceId?: string;
98
+ spanId?: string;
99
+ parentSpanId?: string;
100
+ traceparent?: string;
101
+ };
102
+
103
+ /**
104
+ * Flag definition registered through `defineFlags(...)`.
105
+ */
106
+ export type FlagDef<TValue extends FlagValue = FlagValue> = {
107
+ key: FlagKey;
108
+ kind: FlagKindForValue<TValue>;
109
+ defaultValue: TValue;
110
+ description?: string;
111
+ metadata?: Record<string, unknown>;
112
+ };
113
+
114
+ /**
115
+ * Infer the flag value type from a flag definition.
116
+ */
117
+ export type InferFlagValue<TFlag> =
118
+ TFlag extends FlagDef<infer TValue> ? TValue : never;
119
+
120
+ /**
121
+ * Map a value type to its runtime flag kind.
122
+ */
123
+ export type FlagKindForValue<TValue extends FlagValue> = TValue extends boolean
124
+ ? "boolean"
125
+ : TValue extends string
126
+ ? "string"
127
+ : TValue extends number
128
+ ? "number"
129
+ : "object";
130
+
131
+ /**
132
+ * Options accepted when declaring a flag.
133
+ */
134
+ export type DefineFlagOptions<TValue extends FlagValue> = {
135
+ default: TValue;
136
+ description?: string;
137
+ metadata?: Record<string, unknown>;
138
+ };
139
+
140
+ /**
141
+ * Registry returned by `defineFlags(...)`.
142
+ */
143
+ export type FlagRegistry = Record<string, FlagDef>;
144
+
145
+ /**
146
+ * Reason a flag received its value.
147
+ */
148
+ export type FlagEvaluationReason =
149
+ | "static"
150
+ | "targeting_match"
151
+ | "default"
152
+ | "error"
153
+ | "unknown"
154
+ | (string & {});
155
+
156
+ /**
157
+ * Normalized error summary for failed flag provider evaluations.
158
+ */
159
+ export type FlagEvaluationError = {
160
+ message: string;
161
+ code?: string;
162
+ };
163
+
164
+ /**
165
+ * Options accepted by flag evaluations.
166
+ */
167
+ export type FlagEvaluationOptions<TValue extends FlagValue = FlagValue> = {
168
+ context?: FlagEvaluationContext;
169
+ /**
170
+ * Override the flag declaration default for this call.
171
+ */
172
+ defaultValue?: TValue;
173
+ /**
174
+ * Explicitly record an exposure after evaluation succeeds. Plain evaluation
175
+ * does not record exposure by default.
176
+ */
177
+ expose?: boolean;
178
+ };
179
+
180
+ /**
181
+ * Normalized flag evaluation details.
182
+ */
183
+ export type FlagEvaluationDetails<TValue extends FlagValue = FlagValue> = {
184
+ key: string;
185
+ value: TValue;
186
+ defaultValue: TValue;
187
+ kind: FlagKindForValue<TValue>;
188
+ reason: FlagEvaluationReason;
189
+ defaulted: boolean;
190
+ variant?: string;
191
+ metadata?: Record<string, unknown>;
192
+ error?: FlagEvaluationError;
193
+ };
194
+
195
+ /**
196
+ * Options accepted by explicit exposure recording.
197
+ */
198
+ export type FlagExposureOptions = {
199
+ context?: FlagEvaluationContext;
200
+ value?: FlagValue;
201
+ variant?: string;
202
+ metadata?: Record<string, unknown>;
203
+ };
204
+
205
+ /**
206
+ * Options accepted by tracking calls.
207
+ */
208
+ export type FlagTrackOptions = {
209
+ context?: FlagEvaluationContext;
210
+ value?: number;
211
+ metadata?: Record<string, unknown>;
212
+ };
213
+
214
+ /**
215
+ * App-facing feature flag port.
216
+ */
217
+ export type FlagsPort = {
218
+ evaluate<TValue extends FlagValue>(
219
+ flag: FlagDef<TValue>,
220
+ options?: FlagEvaluationOptions<TValue>,
221
+ ): Promise<TValue>;
222
+ details<TValue extends FlagValue>(
223
+ flag: FlagDef<TValue>,
224
+ options?: FlagEvaluationOptions<TValue>,
225
+ ): Promise<FlagEvaluationDetails<TValue>>;
226
+ recordExposure<TValue extends FlagValue>(
227
+ flag: FlagDef<TValue>,
228
+ options?: FlagExposureOptions,
229
+ ): Promise<void>;
230
+ track(event: string, options?: FlagTrackOptions): Promise<void>;
231
+ };
232
+
233
+ /**
234
+ * Flag evaluation observation emitted by memory/static adapters.
235
+ */
236
+ export type FlagEvaluationObservation<TValue extends FlagValue = FlagValue> = {
237
+ source: "evaluate" | "details";
238
+ flag: FlagDef<TValue>;
239
+ context?: FlagEvaluationContext;
240
+ details: FlagEvaluationDetails<TValue>;
241
+ durationMs: number;
242
+ requestId?: string;
243
+ traceId?: string;
244
+ spanId?: string;
245
+ parentSpanId?: string;
246
+ traceparent?: string;
247
+ };
248
+
249
+ /**
250
+ * Explicit flag exposure observation.
251
+ */
252
+ export type FlagExposureObservation<TValue extends FlagValue = FlagValue> = {
253
+ flag: FlagDef<TValue>;
254
+ context?: FlagEvaluationContext;
255
+ value?: FlagValue;
256
+ variant?: string;
257
+ metadata?: Record<string, unknown>;
258
+ requestId?: string;
259
+ traceId?: string;
260
+ spanId?: string;
261
+ parentSpanId?: string;
262
+ traceparent?: string;
263
+ };
264
+
265
+ /**
266
+ * Flag tracking observation.
267
+ */
268
+ export type FlagTrackObservation = {
269
+ event: string;
270
+ context?: FlagEvaluationContext;
271
+ value?: number;
272
+ metadata?: Record<string, unknown>;
273
+ requestId?: string;
274
+ traceId?: string;
275
+ spanId?: string;
276
+ parentSpanId?: string;
277
+ traceparent?: string;
278
+ };
279
+
280
+ export type FlagEvaluationObserver = (
281
+ observation: FlagEvaluationObservation,
282
+ ) => MaybePromise<void>;
283
+
284
+ export type FlagExposureObserver = (
285
+ observation: FlagExposureObservation,
286
+ ) => MaybePromise<void>;
287
+
288
+ export type FlagTrackObserver = (
289
+ observation: FlagTrackObservation,
290
+ ) => MaybePromise<void>;
291
+
292
+ /**
293
+ * Options for memory/static flag adapters.
294
+ */
295
+ export type CreateFlagsOptions = {
296
+ onEvaluation?: FlagEvaluationObserver;
297
+ onExposure?: FlagExposureObserver;
298
+ onTrack?: FlagTrackObserver;
299
+ };
300
+
301
+ /**
302
+ * Static flag values keyed by flag key.
303
+ */
304
+ export type StaticFlagValues = Record<string, FlagValue>;
305
+
306
+ /**
307
+ * Options for `createStaticFlags(...)`.
308
+ */
309
+ export type CreateStaticFlagsOptions = CreateFlagsOptions & {
310
+ values?: StaticFlagValues;
311
+ };
312
+
313
+ /**
314
+ * In-memory flags port with mutation helpers for tests and local development.
315
+ */
316
+ export type MemoryFlagsPort = FlagsPort & {
317
+ values: Map<string, FlagValue>;
318
+ set<TValue extends FlagValue>(flag: FlagDef<TValue>, value: TValue): void;
319
+ reset(flag?: FlagDef): void;
320
+ };
321
+
322
+ function createFlag<TValue extends FlagValue>(
323
+ kind: FlagKind,
324
+ key: string,
325
+ options: DefineFlagOptions<TValue>,
326
+ ): FlagDef<TValue> {
327
+ const defaultValue = options.default;
328
+ if (!matchesKind(defaultValue, kind)) {
329
+ throw new Error(
330
+ `Flag "${key}" default value does not match "${kind}" flag kind.`,
331
+ );
332
+ }
333
+
334
+ return {
335
+ key,
336
+ kind: kind as FlagKindForValue<TValue>,
337
+ defaultValue,
338
+ description: options.description,
339
+ metadata: options.metadata,
340
+ };
341
+ }
342
+
343
+ function defineBooleanFlag(
344
+ key: string,
345
+ options: DefineFlagOptions<boolean>,
346
+ ): FlagDef<boolean> {
347
+ return createFlag("boolean", key, options);
348
+ }
349
+
350
+ function defineStringFlag(
351
+ key: string,
352
+ options: DefineFlagOptions<string>,
353
+ ): FlagDef<string>;
354
+ function defineStringFlag<const TValue extends string>(
355
+ key: string,
356
+ options: DefineFlagOptions<TValue>,
357
+ ): FlagDef<TValue>;
358
+ function defineStringFlag<TValue extends string>(
359
+ key: string,
360
+ options: DefineFlagOptions<TValue>,
361
+ ): FlagDef<TValue> {
362
+ return createFlag("string", key, options);
363
+ }
364
+
365
+ function defineNumberFlag(
366
+ key: string,
367
+ options: DefineFlagOptions<number>,
368
+ ): FlagDef<number>;
369
+ function defineNumberFlag<const TValue extends number>(
370
+ key: string,
371
+ options: DefineFlagOptions<TValue>,
372
+ ): FlagDef<TValue>;
373
+ function defineNumberFlag<TValue extends number>(
374
+ key: string,
375
+ options: DefineFlagOptions<TValue>,
376
+ ): FlagDef<TValue> {
377
+ return createFlag("number", key, options);
378
+ }
379
+
380
+ function defineObjectFlag<TValue extends FlagObjectValue>(
381
+ key: string,
382
+ options: DefineFlagOptions<TValue>,
383
+ ): FlagDef<TValue> {
384
+ return createFlag("object", key, options);
385
+ }
386
+
387
+ /**
388
+ * Define one feature flag declaration.
389
+ */
390
+ export const defineFlag = {
391
+ boolean: defineBooleanFlag,
392
+ string: defineStringFlag,
393
+ number: defineNumberFlag,
394
+ object: defineObjectFlag,
395
+ };
396
+
397
+ /**
398
+ * Define a typed registry of feature flags.
399
+ */
400
+ export function defineFlags<const TRegistry extends FlagRegistry>(
401
+ registry: TRegistry,
402
+ ): TRegistry {
403
+ return registry;
404
+ }
405
+
406
+ /**
407
+ * Alias for apps that prefer to name the collection explicitly.
408
+ */
409
+ export const defineFlagRegistry = defineFlags;
410
+
411
+ /**
412
+ * Evaluate a flag through any `FlagsPort`.
413
+ */
414
+ export function evaluateFlag<TValue extends FlagValue>(
415
+ flags: FlagsPort,
416
+ flag: FlagDef<TValue>,
417
+ options?: FlagEvaluationOptions<TValue>,
418
+ ): Promise<TValue> {
419
+ return flags.evaluate(flag, options);
420
+ }
421
+
422
+ /**
423
+ * Return detailed flag evaluation information through any `FlagsPort`.
424
+ */
425
+ export function getFlagDetails<TValue extends FlagValue>(
426
+ flags: FlagsPort,
427
+ flag: FlagDef<TValue>,
428
+ options?: FlagEvaluationOptions<TValue>,
429
+ ): Promise<FlagEvaluationDetails<TValue>> {
430
+ return flags.details(flag, options);
431
+ }
432
+
433
+ /**
434
+ * Create static flags backed by fixed values.
435
+ */
436
+ export function createStaticFlags(
437
+ options: CreateStaticFlagsOptions = {},
438
+ ): FlagsPort {
439
+ return createFlagsFromStore(() => options.values ?? {}, options);
440
+ }
441
+
442
+ /**
443
+ * Create mutable in-memory flags for tests, examples, and local development.
444
+ */
445
+ export function createMemoryFlags(
446
+ options: CreateStaticFlagsOptions = {},
447
+ ): MemoryFlagsPort {
448
+ const values = new Map<string, FlagValue>(
449
+ Object.entries(options.values ?? {}),
450
+ );
451
+ const port = createFlagsFromStore(() => Object.fromEntries(values), options);
452
+
453
+ return {
454
+ ...port,
455
+ values,
456
+ set(flag, value) {
457
+ if (!matchesKind(value, flag.kind)) {
458
+ throw new Error(
459
+ `Flag "${flag.key}" value does not match "${flag.kind}" flag kind.`,
460
+ );
461
+ }
462
+ values.set(flag.key, value);
463
+ },
464
+ reset(flag) {
465
+ if (flag) {
466
+ values.delete(flag.key);
467
+ return;
468
+ }
469
+ values.clear();
470
+ },
471
+ };
472
+ }
473
+
474
+ function createFlagsFromStore(
475
+ readValues: () => StaticFlagValues,
476
+ observers: CreateFlagsOptions,
477
+ ): FlagsPort {
478
+ async function resolve<TValue extends FlagValue>(
479
+ flag: FlagDef<TValue>,
480
+ options: FlagEvaluationOptions<TValue> | undefined,
481
+ source: FlagEvaluationObservation["source"],
482
+ ): Promise<FlagEvaluationDetails<TValue>> {
483
+ const startedAt = Date.now();
484
+ const defaultValue = options?.defaultValue ?? flag.defaultValue;
485
+ const values = readValues();
486
+ const stored = values[flag.key];
487
+ const hasStored = Object.hasOwn(values, flag.key);
488
+ const usedStoredValue = hasStored && matchesKind(stored, flag.kind);
489
+ const value = usedStoredValue
490
+ ? (stored as typeof defaultValue)
491
+ : defaultValue;
492
+ const details: FlagEvaluationDetails<typeof defaultValue> = {
493
+ key: flag.key,
494
+ kind: flag.kind as FlagKindForValue<typeof defaultValue>,
495
+ value,
496
+ defaultValue,
497
+ reason: usedStoredValue ? "static" : "default",
498
+ defaulted: !usedStoredValue,
499
+ } satisfies FlagEvaluationDetails<typeof defaultValue>;
500
+
501
+ await observeEvaluation(observers.onEvaluation, {
502
+ source,
503
+ flag,
504
+ context: options?.context,
505
+ details,
506
+ durationMs: Date.now() - startedAt,
507
+ ...correlationFromContext(options?.context),
508
+ });
509
+
510
+ if (options?.expose) {
511
+ await flags.recordExposure(flag, {
512
+ context: options.context,
513
+ value: details.value,
514
+ variant: details.variant,
515
+ metadata: details.metadata,
516
+ });
517
+ }
518
+
519
+ return details;
520
+ }
521
+
522
+ const flags: FlagsPort = {
523
+ async evaluate(flag, options) {
524
+ const details = await resolve(flag, options, "evaluate");
525
+ return details.value;
526
+ },
527
+ async details(flag, options) {
528
+ return resolve(flag, options, "details");
529
+ },
530
+ async recordExposure(flag, options) {
531
+ await observeExposure(observers.onExposure, {
532
+ flag,
533
+ context: options?.context,
534
+ value: options?.value,
535
+ variant: options?.variant,
536
+ metadata: options?.metadata,
537
+ ...correlationFromContext(options?.context),
538
+ });
539
+ },
540
+ async track(event, options) {
541
+ await observeTrack(observers.onTrack, {
542
+ event,
543
+ context: options?.context,
544
+ value: options?.value,
545
+ metadata: options?.metadata,
546
+ ...correlationFromContext(options?.context),
547
+ });
548
+ },
549
+ };
550
+
551
+ return flags;
552
+ }
553
+
554
+ function matchesKind(value: unknown, kind: FlagKind): boolean {
555
+ switch (kind) {
556
+ case "boolean":
557
+ return typeof value === "boolean";
558
+ case "string":
559
+ return typeof value === "string";
560
+ case "number":
561
+ return typeof value === "number" && Number.isFinite(value);
562
+ case "object":
563
+ return typeof value === "object" && value !== null;
564
+ }
565
+ }
566
+
567
+ function correlationFromContext(context: FlagEvaluationContext | undefined) {
568
+ return {
569
+ requestId: context?.requestId,
570
+ traceId: context?.traceId,
571
+ spanId: context?.spanId,
572
+ parentSpanId: context?.parentSpanId,
573
+ traceparent: context?.traceparent,
574
+ };
575
+ }
576
+
577
+ async function observeEvaluation(
578
+ observer: FlagEvaluationObserver | undefined,
579
+ observation: FlagEvaluationObservation,
580
+ ): Promise<void> {
581
+ try {
582
+ await observer?.(observation);
583
+ } catch {
584
+ // Observers are diagnostic only.
585
+ }
586
+ }
587
+
588
+ async function observeExposure(
589
+ observer: FlagExposureObserver | undefined,
590
+ observation: FlagExposureObservation,
591
+ ): Promise<void> {
592
+ try {
593
+ await observer?.(observation);
594
+ } catch {
595
+ // Observers are diagnostic only.
596
+ }
597
+ }
598
+
599
+ async function observeTrack(
600
+ observer: FlagTrackObserver | undefined,
601
+ observation: FlagTrackObservation,
602
+ ): Promise<void> {
603
+ try {
604
+ await observer?.(observation);
605
+ } catch {
606
+ // Observers are diagnostic only.
607
+ }
608
+ }