@toggld/node 0.1.0 → 0.3.0

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as FlagMetadata, i as EvaluationContext, n as ToggldClientOptions, o as FlagValue, r as ErrorCode, s as Reason, t as EvaluationDetails } from "./types-RmWNnA7-.mjs";
1
+ import { a as ErrorCode, c as FlagValue, i as ToggldClientOptions, l as Reason, n as EvaluationMode, o as EvaluationContext, r as SyncOptions, s as FlagMetadata, t as EvaluationDetails } from "./types-CPtRV5-5.mjs";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  /**
@@ -9,8 +9,19 @@ import { a as FlagMetadata, i as EvaluationContext, n as ToggldClientOptions, o
9
9
  declare class ToggldClient {
10
10
  #private;
11
11
  private constructor();
12
- /** Create a client. Resolves once the underlying runtime is ready. */
12
+ /**
13
+ * Create a client. Resolves once the underlying runtime is ready — and, in
14
+ * local mode, once the first flag snapshot has synced (or `readyTimeoutMs`,
15
+ * default 5 s, has elapsed; the client then serves caller defaults with
16
+ * `PROVIDER_NOT_READY` while sync keeps retrying in the background).
17
+ */
13
18
  static create(options: ToggldClientOptions): Promise<ToggldClient>;
19
+ /**
20
+ * Resolves once the client has flag data to evaluate with (immediately in
21
+ * remote mode). Useful if `create` resolved on the ready timeout and you
22
+ * want to await sync explicitly.
23
+ */
24
+ ready(): Promise<void>;
14
25
  /** Resolve a flag, returning full details with an untyped (JSON) value. */
15
26
  evaluate(key: string, defaultValue: FlagValue, context?: EvaluationContext): Promise<EvaluationDetails<FlagValue>>;
16
27
  /** Resolve every flag for the environment in one request (OFREP bulk). */
@@ -27,4 +38,4 @@ declare class ToggldClient {
27
38
  close(): Promise<void>;
28
39
  }
29
40
  //#endregion
30
- export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type FlagMetadata, type FlagValue, type Reason, ToggldClient, type ToggldClientOptions };
41
+ export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type EvaluationMode, type FlagMetadata, type FlagValue, type Reason, type SyncOptions, ToggldClient, type ToggldClientOptions };
package/dist/index.mjs CHANGED
@@ -1,23 +1,27 @@
1
- import { n as layer, t as ToggldClient$1 } from "./core-zMjaOru1.mjs";
2
- import { ManagedRuntime } from "effect";
1
+ import { n as layer, t as ToggldClient$1 } from "./core-DBVGNtur.mjs";
2
+ import { Duration, Effect, ManagedRuntime } from "effect";
3
3
  //#region src/index.ts
4
4
  /**
5
5
  * `@toggld/node` — the default entry: a plain Promise API.
6
6
  *
7
- * Remote flag evaluation over the server's OFREP endpoints, with **no Effect in
8
- * any signature**. Internally it runs the Effect implementation from `./effect`
9
- * under a `ManagedRuntime`.
7
+ * Flag evaluation with **no Effect in any signature**. Internally it runs the
8
+ * Effect implementation from `./effect` under a `ManagedRuntime`.
10
9
  *
11
10
  * ```ts
12
11
  * import { ToggldClient } from "@toggld/node"
13
12
  *
14
- * const client = await ToggldClient.create({ sdkKey, endpoint })
13
+ * // Local evaluation (recommended for long-lived servers): flag definitions
14
+ * // sync in the background and every evaluation is in-process — microseconds,
15
+ * // zero server requests.
16
+ * const client = await ToggldClient.create({ sdkKey, endpoint, mode: "local" })
15
17
  * const on = await client.getBooleanValue("new-checkout", false, { targetingKey: "user-123" })
16
18
  * await client.close()
17
19
  * ```
18
20
  *
19
- * Evaluation never throws: any failure resolves to the supplied default value,
20
- * with the reason/error available via the `*Details` methods.
21
+ * Omitting `mode` keeps the default **remote** evaluation (each call asks the
22
+ * flag API over OFREP). Evaluation never throws: any failure resolves to the
23
+ * supplied default value, with the reason/error available via the `*Details`
24
+ * methods.
21
25
  */
22
26
  /**
23
27
  * A Toggld client backed by a private Effect runtime. Create one with {@link
@@ -31,10 +35,26 @@ var ToggldClient = class ToggldClient {
31
35
  this.#runtime = runtime;
32
36
  this.#client = client;
33
37
  }
34
- /** Create a client. Resolves once the underlying runtime is ready. */
38
+ /**
39
+ * Create a client. Resolves once the underlying runtime is ready — and, in
40
+ * local mode, once the first flag snapshot has synced (or `readyTimeoutMs`,
41
+ * default 5 s, has elapsed; the client then serves caller defaults with
42
+ * `PROVIDER_NOT_READY` while sync keeps retrying in the background).
43
+ */
35
44
  static async create(options) {
36
45
  const runtime = ManagedRuntime.make(layer(options));
37
- return new ToggldClient(runtime, await runtime.runPromise(ToggldClient$1));
46
+ const client = await runtime.runPromise(ToggldClient$1);
47
+ const readyTimeoutMs = options.sync?.readyTimeoutMs ?? 5e3;
48
+ await runtime.runPromise(client.ready.pipe(Effect.timeout(Duration.millis(readyTimeoutMs)), Effect.ignore));
49
+ return new ToggldClient(runtime, client);
50
+ }
51
+ /**
52
+ * Resolves once the client has flag data to evaluate with (immediately in
53
+ * remote mode). Useful if `create` resolved on the ready timeout and you
54
+ * want to await sync explicitly.
55
+ */
56
+ ready() {
57
+ return this.#runtime.runPromise(this.#client.ready);
38
58
  }
39
59
  /** Resolve a flag, returning full details with an untyped (JSON) value. */
40
60
  evaluate(key, defaultValue, context) {
@@ -1,16 +1,4 @@
1
- import { Context, Duration, Effect, Layer, Schema } from "effect";
2
- import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
- //#region ../spec/src/index.ts
4
- /** True when `value` is assignable to a flag declared as `type`. */
5
- const valueMatchesType = (value, type) => {
6
- switch (type) {
7
- case "boolean": return typeof value === "boolean";
8
- case "string": return typeof value === "string";
9
- case "number": return typeof value === "number" && Number.isFinite(value);
10
- case "object": return typeof value === "object" && value !== null;
11
- }
12
- };
13
- //#endregion
1
+ import { Schema } from "effect";
14
2
  //#region ../schema/src/audit.ts
15
3
  /**
16
4
  * Audit log wire contract (ADR-0009). Every management/control-plane mutation is
@@ -36,6 +24,9 @@ const AuditAction = Schema.Literals([
36
24
  "flag.create",
37
25
  "flag.update",
38
26
  "flag.delete",
27
+ "segment.create",
28
+ "segment.update",
29
+ "segment.delete",
39
30
  "environment.create",
40
31
  "environment.delete",
41
32
  "apiKey.create",
@@ -46,6 +37,7 @@ const AuditAction = Schema.Literals([
46
37
  /** The kind of resource an event concerns; pairs with `resourceId`. */
47
38
  const AuditResourceType = Schema.Literals([
48
39
  "flag",
40
+ "segment",
49
41
  "environment",
50
42
  "apiKey",
51
43
  "tenant"
@@ -102,6 +94,15 @@ const CreateEnvironmentInput = Schema.Struct({
102
94
  key: Schema.String.check(Schema.isMinLength(1)),
103
95
  name: Schema.String.check(Schema.isMinLength(1))
104
96
  });
97
+ Schema.Struct({
98
+ id: Schema.String,
99
+ tenantId: Schema.String,
100
+ /** The single environment this token is scoped to. */
101
+ environmentId: Schema.String,
102
+ /** The full token value — intentionally exposed; it is non-secret. */
103
+ token: Schema.String,
104
+ createdAt: Schema.String
105
+ });
105
106
  /**
106
107
  * Public view of an API key — safe to list. Never includes the secret token or
107
108
  * its hash; only a non-secret prefix for human identification.
@@ -232,6 +233,29 @@ Schema.ErrorClass("ApiKeyNotFound")({
232
233
  keyId: Schema.String,
233
234
  message: Schema.String
234
235
  });
236
+ Schema.ErrorClass("SegmentNotFound")({
237
+ _tag: Schema.tag("SegmentNotFound"),
238
+ segmentKey: Schema.String,
239
+ message: Schema.String
240
+ });
241
+ Schema.ErrorClass("SegmentAlreadyExists")({
242
+ _tag: Schema.tag("SegmentAlreadyExists"),
243
+ segmentKey: Schema.String,
244
+ message: Schema.String
245
+ });
246
+ Schema.ErrorClass("SegmentValidationError")({
247
+ _tag: Schema.tag("SegmentValidationError"),
248
+ segmentKey: Schema.String,
249
+ issues: Schema.Array(Schema.String),
250
+ message: Schema.String
251
+ });
252
+ Schema.ErrorClass("SegmentReferenced")({
253
+ _tag: Schema.tag("SegmentReferenced"),
254
+ segmentKey: Schema.String,
255
+ /** Keys of the flags whose targeting still references the segment. */
256
+ flagKeys: Schema.Array(Schema.String),
257
+ message: Schema.String
258
+ });
235
259
  //#endregion
236
260
  //#region ../schema/src/flag.ts
237
261
  /**
@@ -287,12 +311,19 @@ const ServeRollout = Schema.Struct({
287
311
  splits: Schema.Array(Split)
288
312
  });
289
313
  const Serve = Schema.Union([ServeVariant, ServeRollout]);
314
+ /** A reference to a reusable {@link Segment} from a targeting rule (ADR-0006). */
315
+ const SegmentRef = Schema.Struct({
316
+ segment: Schema.String,
317
+ negate: Schema.optionalKey(Schema.Boolean)
318
+ });
290
319
  /** An ordered targeting rule. The first rule whose conditions all match wins. */
291
320
  const Rule = Schema.Struct({
292
321
  id: Schema.String,
293
322
  description: Schema.optionalKey(Schema.String),
294
323
  /** ANDed conditions. An empty list always matches (useful as a catch-all). */
295
324
  conditions: Schema.Array(Condition),
325
+ /** Reusable segments, ANDed with `conditions` (ADR-0006). */
326
+ segments: Schema.optionalKey(Schema.Array(SegmentRef)),
296
327
  serve: Serve
297
328
  });
298
329
  /**
@@ -320,6 +351,35 @@ const Flag = Schema.Struct({
320
351
  createdAt: Schema.String,
321
352
  updatedAt: Schema.String
322
353
  });
354
+ /** How a segment's enumerated member list is distributed for evaluation. */
355
+ const SegmentListMode = Schema.Literals([
356
+ "none",
357
+ "inline",
358
+ "remote"
359
+ ]);
360
+ /** A single segment membership rule: ANDed conditions, no `serve` (ADR-0006). */
361
+ const SegmentRule = Schema.Struct({
362
+ id: Schema.String,
363
+ description: Schema.optionalKey(Schema.String),
364
+ conditions: Schema.Array(Condition)
365
+ });
366
+ /**
367
+ * A reusable audience definition as it rides the sync bundle (ADR-0006). Inline
368
+ * member keys are present only for `inline` segments; `remote` segments omit
369
+ * them and resolve via a backing-store lookup.
370
+ */
371
+ const Segment = Schema.Struct({
372
+ key: Schema.String,
373
+ name: Schema.optionalKey(Schema.String),
374
+ description: Schema.optionalKey(Schema.String),
375
+ keyAttribute: Schema.String,
376
+ rules: Schema.Array(SegmentRule),
377
+ listMode: SegmentListMode,
378
+ members: Schema.optionalKey(Schema.Array(Schema.String)),
379
+ version: Schema.Number,
380
+ createdAt: Schema.String,
381
+ updatedAt: Schema.String
382
+ });
323
383
  /**
324
384
  * Freeform organization labels for a flag (ADR-0008). A flag may carry any
325
385
  * number; they group/filter flags in the dashboard and have **no** effect on
@@ -433,117 +493,79 @@ Schema.Struct({
433
493
  metadata: Schema.optionalKey(FlagMetadata),
434
494
  tags: Schema.optionalKey(Tags)
435
495
  });
496
+ Schema.Struct({
497
+ key: Schema.String,
498
+ name: Schema.optionalKey(Schema.String),
499
+ description: Schema.optionalKey(Schema.String),
500
+ keyAttribute: Schema.String,
501
+ rules: Schema.Array(SegmentRule),
502
+ listMode: Segment.fields.listMode,
503
+ /** Number of enumerated members in the list (0 for a rules-only segment). */
504
+ memberCount: Schema.Number,
505
+ version: Schema.Number,
506
+ /** Keys of the flags whose targeting references this segment, in any environment. */
507
+ usedBy: Schema.Array(Schema.String),
508
+ createdAt: Schema.String,
509
+ updatedAt: Schema.String
510
+ });
511
+ Schema.Struct({
512
+ key: Schema.String.check(Schema.isMinLength(1)),
513
+ name: Schema.optionalKey(Schema.String),
514
+ description: Schema.optionalKey(Schema.String),
515
+ /** Defaults to `targetingKey` server-side when omitted. */
516
+ keyAttribute: Schema.optionalKey(Schema.String),
517
+ rules: Schema.optionalKey(Schema.Array(SegmentRule))
518
+ });
519
+ Schema.Struct({
520
+ name: Schema.optionalKey(Schema.String),
521
+ description: Schema.optionalKey(Schema.String),
522
+ keyAttribute: Schema.optionalKey(Schema.String),
523
+ rules: Schema.optionalKey(Schema.Array(SegmentRule))
524
+ });
525
+ Schema.Struct({
526
+ add: Schema.optionalKey(Schema.Array(Schema.String)),
527
+ remove: Schema.optionalKey(Schema.Array(Schema.String))
528
+ });
529
+ /** One enumerated member of a segment's list. */
530
+ const SegmentMember = Schema.Struct({
531
+ key: Schema.String,
532
+ addedAt: Schema.String
533
+ });
534
+ Schema.Struct({
535
+ members: Schema.Array(SegmentMember),
536
+ /** Total members matching the (optional) query — the count line in the UI. */
537
+ total: Schema.Number
538
+ });
436
539
  //#endregion
437
- //#region src/internal/core.ts
540
+ //#region ../schema/src/sync.ts
438
541
  /**
439
- * The Effect core of the Toggld Node SDK — a remote-evaluation client over the
440
- * server's OFREP endpoints.
441
- *
442
- * This is the canonical implementation; `../index.ts` (the plain Promise API) is
443
- * a thin facade that runs this under a `ManagedRuntime`. The SDK performs **no
444
- * local evaluation** — every call hits the flag API, which evaluates the flag
445
- * against the supplied context and returns the resolved value (OpenFeature
446
- * "remote evaluation"). Local in-process evaluation via `@toggld/engine` is a
447
- * later milestone (see ADR-0001).
542
+ * Definition-sync wire schemas (ADR-0001 §4): the versioned full snapshot the
543
+ * sync surface serves to locally-evaluating SDKs, and the messages exchanged on
544
+ * the live sync stream. Like the OFREP schemas, these are shared by the server
545
+ * (producer) and `@toggld/node` (consumer) so the contract cannot drift.
448
546
  *
449
- * OpenFeature semantics: evaluation never throws. Any failure — network error,
450
- * timeout, unknown flag, a server-reported error, or a value whose type doesn't
451
- * match the requested getter — resolves to the caller's default value with the
452
- * details carrying `reason: "ERROR"` and an `errorCode`.
547
+ * `version` is opaque and equality-compared (ADR-0005 phase 1); the ordered
548
+ * `config_version` lands in this same field later without a wire change.
453
549
  */
454
- const DEFAULT_TIMEOUT_MS = 5e3;
455
- const isBoolean = (v) => typeof v === "boolean";
456
- const isString = (v) => typeof v === "string";
457
- const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
458
- const isObject = (v) => valueMatchesType(v, "object");
459
- const okDetails = (key, s) => ({
460
- flagKey: key,
461
- value: s.value,
462
- variant: s.variant,
463
- reason: s.reason,
464
- errorCode: void 0,
465
- errorMessage: void 0,
466
- flagMetadata: s.metadata ?? {}
467
- });
468
- const wireErrorDetails = (fallback, e) => ({
469
- flagKey: e.key,
470
- value: fallback,
471
- variant: void 0,
472
- reason: "ERROR",
473
- errorCode: e.errorCode,
474
- errorMessage: e.errorDetails,
475
- flagMetadata: e.metadata ?? {}
476
- });
477
- const errorDetails = (key, value, errorCode, errorMessage, flagMetadata = {}) => ({
478
- flagKey: key,
479
- value,
480
- variant: void 0,
481
- reason: "ERROR",
482
- errorCode,
483
- errorMessage,
484
- flagMetadata
485
- });
486
- /** A readable message for an arbitrary thrown/failed value. */
487
- const describe = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" ? e.message : String(e);
488
- /** Map a transport/decoding failure to an OpenFeature error code. */
489
- const codeFor = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag === "SchemaError" ? "PARSE_ERROR" : "GENERAL";
490
- /**
491
- * The Toggld client as an Effect service. Build a {@link layer} from options and
492
- * compose it into your runtime, then `yield* ToggldClient` to evaluate flags.
493
- */
494
- var ToggldClient = class extends Context.Service()("@toggld/node/ToggldClient") {};
495
- const make = (options) => Effect.gen(function* () {
496
- const base = yield* HttpClient.HttpClient;
497
- const timeout = Duration.millis(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
498
- const http = base.pipe(HttpClient.mapRequest(HttpClientRequest.prependUrl(options.endpoint)), HttpClient.mapRequest(HttpClientRequest.bearerToken(options.sdkKey)));
499
- const mergedContext = (context) => ({
500
- ...options.defaultContext,
501
- ...context
502
- });
503
- const decode = HttpClientResponse.schemaBodyJson;
504
- const evaluate = (key, defaultValue, context) => Effect.gen(function* () {
505
- const request = HttpClientRequest.post(`/ofrep/v1/evaluate/flags/${encodeURIComponent(key)}`).pipe(HttpClientRequest.bodyJsonUnsafe({ context: mergedContext(context) }));
506
- const response = yield* http.execute(request);
507
- return yield* HttpClientResponse.matchStatus(response, {
508
- "2xx": (r) => Effect.map(decode(OfrepSuccess)(r), (s) => okDetails(key, s)),
509
- "4xx": (r) => Effect.map(decode(OfrepFailure)(r), (e) => wireErrorDetails(defaultValue, e)),
510
- orElse: (r) => Effect.succeed(errorDetails(key, defaultValue, "GENERAL", `Unexpected response status ${r.status}`))
511
- });
512
- }).pipe(Effect.timeout(timeout), Effect.catch((error) => Effect.succeed(errorDetails(key, defaultValue, codeFor(error), describe(error)))));
513
- const evaluateAll = (context) => Effect.gen(function* () {
514
- const request = HttpClientRequest.post("/ofrep/v1/evaluate/flags").pipe(HttpClientRequest.bodyJsonUnsafe({ context: mergedContext(context) }));
515
- const response = yield* http.execute(request);
516
- return (yield* HttpClientResponse.matchStatus(response, {
517
- "2xx": (r) => decode(OfrepBulkResponse)(r),
518
- orElse: (r) => Effect.fail(/* @__PURE__ */ new Error(`Unexpected response status ${r.status}`))
519
- })).flags.map((entry) => "errorCode" in entry ? wireErrorDetails(null, entry) : okDetails(entry.key, entry));
520
- }).pipe(Effect.timeout(timeout), Effect.catch(() => Effect.succeed([])));
521
- /** Resolve, then enforce the requested value type (else `TYPE_MISMATCH`). */
522
- const typed = (key, defaultValue, context, type, guard) => Effect.map(evaluate(key, defaultValue, context), (d) => {
523
- if (d.errorCode !== void 0) return d;
524
- if (guard(d.value)) return d;
525
- return errorDetails(key, defaultValue, "TYPE_MISMATCH", `Flag '${key}' value is not of type ${type}`, d.flagMetadata);
526
- });
527
- const valueOf = (details) => Effect.map(details, (d) => d.value);
528
- return {
529
- evaluate,
530
- evaluateAll,
531
- getBooleanDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "boolean", isBoolean),
532
- getBooleanValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "boolean", isBoolean)),
533
- getStringDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "string", isString),
534
- getStringValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "string", isString)),
535
- getNumberDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "number", isNumber),
536
- getNumberValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "number", isNumber)),
537
- getObjectDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "object", isObject),
538
- getObjectValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "object", isObject))
539
- };
540
- });
541
- /** The HttpClient layer, optionally backed by a caller-supplied `fetch`. */
542
- const httpLayer = (fetchImpl) => fetchImpl ? FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchImpl))) : FetchHttpClient.layer;
550
+ /** A versioned full snapshot of one environment's resolved flag set. */
551
+ const FlagSnapshot = Schema.Struct({
552
+ version: Schema.String,
553
+ flags: Schema.Array(Flag),
554
+ /** Segment definitions referenced by the flags (ADR-0006). */
555
+ segments: Schema.optionalKey(Schema.Array(Segment))
556
+ });
557
+ /** Server→client: the current snapshot (connect greeting and change pushes). */
558
+ const SyncSnapshotMessage = Schema.Struct({
559
+ type: Schema.Literal("snapshot"),
560
+ version: Schema.String,
561
+ flags: Schema.Array(Flag),
562
+ segments: Schema.optionalKey(Schema.Array(Segment))
563
+ });
543
564
  /**
544
- * A self-contained `Layer` providing {@link ToggldClient} — it wires its own
545
- * HTTP backend, so Effect users need only provide this one layer.
565
+ * Every server→client sync-stream message. A single-member union today; the
566
+ * `type` discriminant is the extension point.
546
567
  */
547
- const layer = (options) => Layer.effect(ToggldClient, make(options)).pipe(Layer.provide(httpLayer(options.fetch)));
568
+ const SyncServerMessage = Schema.Union([SyncSnapshotMessage]);
569
+ Schema.Struct({ type: Schema.Literal("resync") });
548
570
  //#endregion
549
- export { layer as n, ToggldClient as t };
571
+ export { OfrepSuccess as a, OfrepFailure as i, SyncServerMessage as n, OfrepBulkResponse as r, FlagSnapshot as t };
@@ -0,0 +1,163 @@
1
+ //#region ../spec/src/index.d.ts
2
+ /**
3
+ * A resolved flag value. OpenFeature permits `boolean | string | number |
4
+ * structure`; we model the union as JSON, which also covers arrays and null for
5
+ * "object" typed flags.
6
+ */
7
+ type FlagValue = null | boolean | number | string | ReadonlyArray<FlagValue> | {
8
+ readonly [key: string]: FlagValue;
9
+ };
10
+ /**
11
+ * Flag metadata: "arbitrary properties, with keys of type string, and values of
12
+ * type boolean, string, or number." (Requirement 2.2.10)
13
+ */
14
+ type MetadataValue = string | number | boolean;
15
+ type FlagMetadata = {
16
+ readonly [key: string]: MetadataValue;
17
+ };
18
+ /**
19
+ * An OpenFeature evaluation context: an optional `targetingKey` identifying the
20
+ * subject (Requirement 3.1.1) plus arbitrary custom JSON-valued attributes
21
+ * (Requirement 3.1.2).
22
+ */
23
+ type EvaluationContext = {
24
+ readonly [attribute: string]: FlagValue;
25
+ };
26
+ /**
27
+ * Resolution reasons. (Requirement 2.2.5)
28
+ */
29
+ type Reason = "STATIC" | "DEFAULT" | "TARGETING_MATCH" | "SPLIT" | "CACHED" | "DISABLED" | "UNKNOWN" | "STALE" | "ERROR";
30
+ /**
31
+ * OpenFeature error codes.
32
+ * @see https://openfeature.dev/specification/types
33
+ */
34
+ type ErrorCode = "PROVIDER_NOT_READY" | "PROVIDER_FATAL" | "FLAG_NOT_FOUND" | "PARSE_ERROR" | "TYPE_MISMATCH" | "TARGETING_KEY_MISSING" | "INVALID_CONTEXT" | "GENERAL";
35
+ //#endregion
36
+ //#region src/internal/types.d.ts
37
+ /**
38
+ * How the client resolves flags.
39
+ *
40
+ * - `"remote"` — every evaluation is an HTTP request to the flag API's OFREP
41
+ * endpoints; the server evaluates and returns the value.
42
+ * - `"local"` — the client backgrounds a **definition sync** (a snapshot fetch
43
+ * plus a live WebSocket stream with polling fallback) and evaluates flags
44
+ * **in-process** with the bundled engine: zero server requests per
45
+ * evaluation, microsecond latency, identical resolutions to the server. The
46
+ * mode for long-lived servers.
47
+ */
48
+ type EvaluationMode = "remote" | "local";
49
+ /** Tuning for the background definition sync (`mode: "local"`). */
50
+ interface SyncOptions {
51
+ /**
52
+ * How updates reach the client.
53
+ *
54
+ * - `"ws"` (default) — a live WebSocket stream (`/sync/v1/stream`) pushes a
55
+ * fresh snapshot on every change, with the snapshot poll below as a
56
+ * safety net (it also serves runtimes where the stream is unavailable —
57
+ * the client degrades to polling automatically).
58
+ * - `"poll"` — no stream; only the periodic snapshot poll.
59
+ */
60
+ readonly transport?: "ws" | "poll";
61
+ /**
62
+ * Interval between snapshot polls in milliseconds. Default `30000`. Until
63
+ * the first snapshot arrives, the client retries faster (exponential from
64
+ * 250 ms) so cold starts aren't gated on this interval.
65
+ */
66
+ readonly pollIntervalMs?: number;
67
+ /**
68
+ * How long `ToggldClient.create` waits for the first snapshot before
69
+ * resolving anyway, in milliseconds. Default `5000`. A client that isn't
70
+ * ready evaluates to caller defaults (`PROVIDER_NOT_READY`) while sync keeps
71
+ * retrying in the background.
72
+ */
73
+ readonly readyTimeoutMs?: number;
74
+ /**
75
+ * Custom `WebSocket` constructor for the stream transport. Defaults to the
76
+ * global `WebSocket` (Node ≥ 22). Useful for tests or proxies.
77
+ */
78
+ readonly webSocketConstructor?: typeof globalThis.WebSocket;
79
+ /**
80
+ * Evaluate from a local snapshot **file** instead of syncing over the network
81
+ * (ADR-0013) — for local development, offline runs, CI, or pinning a known
82
+ * ruleset. The file is a `FlagSnapshot` on disk; generate one with `npx
83
+ * @toggld/node pull`. A file-backed client makes no network requests and needs
84
+ * no reachable endpoint, key, or stream.
85
+ *
86
+ * The path is resolved highest-precedence first:
87
+ *
88
+ * 1. the `TOGGLD_FLAGS_FILE` environment variable (always wins, so a deployed
89
+ * app can be redirected at a file with no code change),
90
+ * 2. this option when given a string path,
91
+ * 3. `./toggld.flags.json` when this is `true`.
92
+ *
93
+ * The file is **watched**: edits are adopted live, no restart. The poll
94
+ * interval above doubles as a re-read safety net (and picks the file up if it
95
+ * is created after the client starts).
96
+ */
97
+ readonly file?: string | boolean;
98
+ }
99
+ /** Configuration for a {@link https://toggld.io Toggld} client. */
100
+ interface ToggldClientOptions {
101
+ /**
102
+ * An **evaluation** (or management) API key, sent as `Authorization: Bearer
103
+ * <sdkKey>`. The key is scoped to one tenant + environment server-side, so a
104
+ * client always evaluates against exactly that environment. Network local
105
+ * evaluation requires a secret key (`ffk_…`) — public browser tokens cannot
106
+ * read flag definitions.
107
+ *
108
+ * Optional **only** for a file-backed client ({@link SyncOptions.file}), which
109
+ * reads its snapshot from disk and never authenticates. Required for remote
110
+ * and network-synced local evaluation.
111
+ */
112
+ readonly sdkKey?: string;
113
+ /**
114
+ * Base URL of the flag API, e.g. `https://toggld-api.example.workers.dev` or
115
+ * `http://localhost:3000` in local development. No trailing path — the client
116
+ * appends the OFREP and sync routes (`/ofrep/v1/...`, `/sync/v1/...`).
117
+ *
118
+ * Optional **only** for a file-backed client ({@link SyncOptions.file}); it
119
+ * makes no requests. Required for remote and network-synced local evaluation.
120
+ */
121
+ readonly endpoint?: string;
122
+ /**
123
+ * Evaluation mode. Defaults to `"remote"`; prefer `"local"` on long-lived
124
+ * servers. See {@link EvaluationMode}.
125
+ */
126
+ readonly mode?: EvaluationMode;
127
+ /** Background sync tuning for `mode: "local"`. Ignored in remote mode. */
128
+ readonly sync?: SyncOptions;
129
+ /**
130
+ * Attributes merged into the evaluation context of **every** request, under
131
+ * any per-call context (per-call attributes win on key collision). Handy for
132
+ * process-wide dimensions such as `region` or `serviceVersion`.
133
+ */
134
+ readonly defaultContext?: EvaluationContext;
135
+ /** Per-request timeout in milliseconds. Default `5000`. */
136
+ readonly timeoutMs?: number;
137
+ /**
138
+ * Custom `fetch` implementation. Defaults to the global `fetch`. Useful for
139
+ * tests, proxies, or injecting retries/instrumentation.
140
+ */
141
+ readonly fetch?: typeof globalThis.fetch;
142
+ }
143
+ /**
144
+ * The outcome of evaluating a single flag — OpenFeature "evaluation details"
145
+ * with a value typed to the requested flag type. Mirrors {@link
146
+ * https://openfeature.dev/specification/sections/evaluation-details}.
147
+ *
148
+ * On any abnormal evaluation (network failure, unknown flag, type mismatch, a
149
+ * server-reported error) the call still resolves: `value` holds the caller's
150
+ * default, `reason` is `"ERROR"`, and `errorCode`/`errorMessage` describe what
151
+ * went wrong. Evaluation never throws.
152
+ */
153
+ interface EvaluationDetails<T extends FlagValue> {
154
+ readonly flagKey: string;
155
+ readonly value: T;
156
+ readonly variant: string | undefined;
157
+ readonly reason: Reason;
158
+ readonly errorCode: ErrorCode | undefined;
159
+ readonly errorMessage: string | undefined;
160
+ readonly flagMetadata: FlagMetadata;
161
+ }
162
+ //#endregion
163
+ export { ErrorCode as a, FlagValue as c, ToggldClientOptions as i, Reason as l, EvaluationMode as n, EvaluationContext as o, SyncOptions as r, FlagMetadata as s, EvaluationDetails as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toggld/node",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Toggld SDK for Node.js — local flag evaluation with a background definition sync. Plain Promise API by default; native Effect API via @toggld/node/effect.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://toggld.io",
@@ -22,6 +22,9 @@
22
22
  "files": [
23
23
  "dist"
24
24
  ],
25
+ "bin": {
26
+ "toggld": "./dist/cli.mjs"
27
+ },
25
28
  "exports": {
26
29
  "./package.json": "./package.json",
27
30
  ".": {
@@ -41,8 +44,11 @@
41
44
  },
42
45
  "devDependencies": {
43
46
  "tsdown": "^0.22.2",
47
+ "typescript": "5.8.3",
44
48
  "unrun": "^0.3.1",
49
+ "@toggld/engine": "0.1.0",
45
50
  "@toggld/schema": "0.1.0",
51
+ "@toggld/server": "0.1.0",
46
52
  "@toggld/spec": "0.1.0"
47
53
  },
48
54
  "scripts": {