@toggld/node 0.2.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/effect.d.mts CHANGED
@@ -1,4 +1,4 @@
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-DItF1mIU.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
  import { Context, Effect, Layer } from "effect";
3
3
 
4
4
  //#region src/internal/core.d.ts
package/dist/effect.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as layer, t as ToggldClient } from "./core-Bx0PVoEm.mjs";
1
+ import { n as layer, t as ToggldClient } from "./core-DBVGNtur.mjs";
2
2
  export { ToggldClient, layer };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
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-DItF1mIU.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
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as layer, t as ToggldClient$1 } from "./core-Bx0PVoEm.mjs";
1
+ import { n as layer, t as ToggldClient$1 } from "./core-DBVGNtur.mjs";
2
2
  import { Duration, Effect, ManagedRuntime } from "effect";
3
3
  //#region src/index.ts
4
4
  /**
@@ -44,10 +44,8 @@ var ToggldClient = class ToggldClient {
44
44
  static async create(options) {
45
45
  const runtime = ManagedRuntime.make(layer(options));
46
46
  const client = await runtime.runPromise(ToggldClient$1);
47
- if (options.mode === "local") {
48
- const readyTimeoutMs = options.sync?.readyTimeoutMs ?? 5e3;
49
- await runtime.runPromise(client.ready.pipe(Effect.timeout(Duration.millis(readyTimeoutMs)), Effect.ignore));
50
- }
47
+ const readyTimeoutMs = options.sync?.readyTimeoutMs ?? 5e3;
48
+ await runtime.runPromise(client.ready.pipe(Effect.timeout(Duration.millis(readyTimeoutMs)), Effect.ignore));
51
49
  return new ToggldClient(runtime, client);
52
50
  }
53
51
  /**
@@ -0,0 +1,571 @@
1
+ import { Schema } from "effect";
2
+ //#region ../schema/src/audit.ts
3
+ /**
4
+ * Audit log wire contract (ADR-0009). Every management/control-plane mutation is
5
+ * recorded as one append-only {@link AuditEvent}; a tenant reads its own trail
6
+ * over `GET /api/v1/audit-log`. Evaluation (OFREP) reads are deliberately *not*
7
+ * audited — this is a record of configuration *changes*, not traffic.
8
+ */
9
+ /** Who performed the change. See the server's `Actor` for how each id is sourced. */
10
+ const AuditActor = Schema.Struct({
11
+ type: Schema.Literals([
12
+ "apiKey",
13
+ "user",
14
+ "service"
15
+ ]),
16
+ id: Schema.String
17
+ });
18
+ /**
19
+ * Closed vocabulary of audited actions — a closed set (not free text) so filters
20
+ * and downstream exports stay stable as resources are added. `flag.update` covers
21
+ * both replace (PUT) and patch (PATCH); the before/after states show what moved.
22
+ */
23
+ const AuditAction = Schema.Literals([
24
+ "flag.create",
25
+ "flag.update",
26
+ "flag.delete",
27
+ "segment.create",
28
+ "segment.update",
29
+ "segment.delete",
30
+ "environment.create",
31
+ "environment.delete",
32
+ "apiKey.create",
33
+ "apiKey.revoke",
34
+ "tenant.create",
35
+ "tenant.update"
36
+ ]);
37
+ /** The kind of resource an event concerns; pairs with `resourceId`. */
38
+ const AuditResourceType = Schema.Literals([
39
+ "flag",
40
+ "segment",
41
+ "environment",
42
+ "apiKey",
43
+ "tenant"
44
+ ]);
45
+ Schema.Struct({
46
+ id: Schema.String,
47
+ /** Owning tenant; null only for control-plane events (tenant provisioning). */
48
+ tenantId: Schema.NullOr(Schema.String),
49
+ /** The environment touched; null for tenant-wide / definition-level / control-plane events. */
50
+ environmentId: Schema.NullOr(Schema.String),
51
+ actor: AuditActor,
52
+ action: AuditAction,
53
+ resourceType: AuditResourceType,
54
+ /** Flag key, environment id, API-key id, or tenant id. */
55
+ resourceId: Schema.String,
56
+ before: Schema.NullOr(Schema.Unknown),
57
+ after: Schema.NullOr(Schema.Unknown),
58
+ /** Non-authoritative request context (request id, source ip, …); empty in v0. */
59
+ metadata: Schema.Record(Schema.String, Schema.Unknown),
60
+ /** ISO 8601 timestamp the change was recorded. */
61
+ occurredAt: Schema.String
62
+ });
63
+ //#endregion
64
+ //#region ../schema/src/auth.ts
65
+ /**
66
+ * Authorization role carried by an API key.
67
+ *
68
+ * - `management` — full CRUD on the tenant's flags, plus evaluation.
69
+ * - `evaluation` — read-only evaluation (OFREP) of the tenant's flags.
70
+ */
71
+ const Role = Schema.Literals(["management", "evaluation"]);
72
+ Schema.Struct({
73
+ id: Schema.String,
74
+ name: Schema.String,
75
+ createdAt: Schema.String,
76
+ /**
77
+ * Max number of flag *definitions* the tenant may hold; `null` = unlimited.
78
+ * The free tier is 10 (the DB column default). The flag API treats this as an
79
+ * opaque number — the web app owns the plan→limit mapping and pushes changes
80
+ * over the control plane (provision + PATCH). Enforced in `FlagService.create`.
81
+ */
82
+ flagLimit: Schema.NullOr(Schema.Int)
83
+ });
84
+ Schema.Struct({
85
+ id: Schema.String,
86
+ tenantId: Schema.String,
87
+ /** Stable slug, unique within the tenant (e.g. `production`). */
88
+ key: Schema.String,
89
+ name: Schema.String,
90
+ createdAt: Schema.String
91
+ });
92
+ /** Body for creating an environment (POST /admin/v1/tenants/:tenantId/environments). */
93
+ const CreateEnvironmentInput = Schema.Struct({
94
+ key: Schema.String.check(Schema.isMinLength(1)),
95
+ name: Schema.String.check(Schema.isMinLength(1))
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
+ });
106
+ /**
107
+ * Public view of an API key — safe to list. Never includes the secret token or
108
+ * its hash; only a non-secret prefix for human identification.
109
+ */
110
+ const ApiKeyInfo = Schema.Struct({
111
+ id: Schema.String,
112
+ tenantId: Schema.String,
113
+ /** The single environment this key is scoped to. */
114
+ environmentId: Schema.String,
115
+ name: Schema.optionalKey(Schema.String),
116
+ role: Role,
117
+ prefix: Schema.String,
118
+ createdAt: Schema.String
119
+ });
120
+ Schema.Struct({
121
+ ...ApiKeyInfo.fields,
122
+ token: Schema.String
123
+ });
124
+ Schema.Struct({
125
+ id: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),
126
+ name: Schema.String.check(Schema.isMinLength(1)),
127
+ environments: Schema.optionalKey(Schema.Array(CreateEnvironmentInput)),
128
+ /** Flag cap for the new tenant; omitted → the free-tier default (10). */
129
+ flagLimit: Schema.optionalKey(Schema.Int)
130
+ });
131
+ Schema.Struct({
132
+ name: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),
133
+ flagLimit: Schema.optionalKey(Schema.NullOr(Schema.Int))
134
+ });
135
+ Schema.Struct({
136
+ role: Role,
137
+ name: Schema.optionalKey(Schema.String),
138
+ environment: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1)))
139
+ });
140
+ //#endregion
141
+ //#region ../schema/src/value.ts
142
+ /**
143
+ * The OpenFeature flag value types. Every flag declares one of these; variant
144
+ * values and resolved values are validated against it.
145
+ *
146
+ * @see https://openfeature.dev/specification/types
147
+ */
148
+ const FlagType = Schema.Literals([
149
+ "boolean",
150
+ "string",
151
+ "number",
152
+ "object"
153
+ ]);
154
+ /**
155
+ * A resolved flag value. OpenFeature permits `boolean | string | number |
156
+ * structure`; we store the union as JSON, which also covers arrays and null
157
+ * for "object" typed flags.
158
+ */
159
+ const FlagValue = Schema.Json;
160
+ /**
161
+ * Flag metadata: "a structure supporting the definition of arbitrary
162
+ * properties, with keys of type string, and values of type boolean, string,
163
+ * or number." (Requirement 2.2.10)
164
+ */
165
+ const MetadataValue = Schema.Union([
166
+ Schema.String,
167
+ Schema.Finite,
168
+ Schema.Boolean
169
+ ]);
170
+ const FlagMetadata = Schema.Record(Schema.String, MetadataValue);
171
+ //#endregion
172
+ //#region ../schema/src/context.ts
173
+ /**
174
+ * An OpenFeature evaluation context: an optional `targetingKey` "identifying
175
+ * the subject of the flag evaluation" (Requirement 3.1.1) plus arbitrary
176
+ * custom fields (Requirement 3.1.2). We accept any JSON-valued attributes.
177
+ */
178
+ const EvaluationContext = Schema.Record(Schema.String, FlagValue);
179
+ Schema.ErrorClass("FlagNotFound")({
180
+ _tag: Schema.tag("FlagNotFound"),
181
+ flagKey: Schema.String,
182
+ message: Schema.String
183
+ });
184
+ Schema.ErrorClass("FlagAlreadyExists")({
185
+ _tag: Schema.tag("FlagAlreadyExists"),
186
+ flagKey: Schema.String,
187
+ message: Schema.String
188
+ });
189
+ Schema.ErrorClass("FlagValidationError")({
190
+ _tag: Schema.tag("FlagValidationError"),
191
+ flagKey: Schema.String,
192
+ issues: Schema.Array(Schema.String),
193
+ message: Schema.String
194
+ });
195
+ Schema.ErrorClass("FlagLimitExceeded")({
196
+ _tag: Schema.tag("FlagLimitExceeded"),
197
+ /** The tenant's current flag cap. */
198
+ limit: Schema.Int,
199
+ /** How many flags the tenant has now (>= limit). */
200
+ current: Schema.Int,
201
+ message: Schema.String
202
+ });
203
+ Schema.ErrorClass("Unauthorized")({
204
+ _tag: Schema.tag("Unauthorized"),
205
+ message: Schema.String
206
+ });
207
+ Schema.ErrorClass("Forbidden")({
208
+ _tag: Schema.tag("Forbidden"),
209
+ message: Schema.String
210
+ });
211
+ Schema.ErrorClass("TenantNotFound")({
212
+ _tag: Schema.tag("TenantNotFound"),
213
+ tenantId: Schema.String,
214
+ message: Schema.String
215
+ });
216
+ Schema.ErrorClass("TenantAlreadyExists")({
217
+ _tag: Schema.tag("TenantAlreadyExists"),
218
+ tenantId: Schema.String,
219
+ message: Schema.String
220
+ });
221
+ Schema.ErrorClass("EnvironmentNotFound")({
222
+ _tag: Schema.tag("EnvironmentNotFound"),
223
+ environment: Schema.String,
224
+ message: Schema.String
225
+ });
226
+ Schema.ErrorClass("EnvironmentAlreadyExists")({
227
+ _tag: Schema.tag("EnvironmentAlreadyExists"),
228
+ environment: Schema.String,
229
+ message: Schema.String
230
+ });
231
+ Schema.ErrorClass("ApiKeyNotFound")({
232
+ _tag: Schema.tag("ApiKeyNotFound"),
233
+ keyId: Schema.String,
234
+ message: Schema.String
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
+ });
259
+ //#endregion
260
+ //#region ../schema/src/flag.ts
261
+ /**
262
+ * Operators usable in a targeting condition. Comparisons coerce sensibly:
263
+ * numeric operators require numbers, string operators require strings, and
264
+ * `in`/`notIn` test membership in an array literal.
265
+ */
266
+ const Operator = Schema.Literals([
267
+ "eq",
268
+ "ne",
269
+ "in",
270
+ "notIn",
271
+ "lt",
272
+ "lte",
273
+ "gt",
274
+ "gte",
275
+ "contains",
276
+ "startsWith",
277
+ "endsWith",
278
+ "exists",
279
+ "notExists"
280
+ ]);
281
+ /**
282
+ * A single targeting condition tested against an attribute of the evaluation
283
+ * context (e.g. `targetingKey`, `email`, `plan`). All conditions in a rule are
284
+ * ANDed together.
285
+ */
286
+ const Condition = Schema.Struct({
287
+ attribute: Schema.String,
288
+ operator: Operator,
289
+ /** Comparison operand. Ignored for `exists`/`notExists`. */
290
+ value: Schema.optionalKey(FlagValue)
291
+ });
292
+ /** A weighted slice of a percentage rollout. */
293
+ const Split = Schema.Struct({
294
+ variant: Schema.String,
295
+ /** Relative weight; the engine normalises weights across all splits. */
296
+ weight: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
297
+ });
298
+ /** Serve a single fixed variant when the rule matches → `TARGETING_MATCH`. */
299
+ const ServeVariant = Schema.Struct({
300
+ type: Schema.Literal("variant"),
301
+ variant: Schema.String
302
+ });
303
+ /**
304
+ * Serve a deterministic percentage rollout when the rule matches → `SPLIT`.
305
+ * Bucketing is consistent for a given subject (`bucketBy`, default
306
+ * `targetingKey`) so a subject always lands in the same slice.
307
+ */
308
+ const ServeRollout = Schema.Struct({
309
+ type: Schema.Literal("rollout"),
310
+ bucketBy: Schema.optionalKey(Schema.String),
311
+ splits: Schema.Array(Split)
312
+ });
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
+ });
319
+ /** An ordered targeting rule. The first rule whose conditions all match wins. */
320
+ const Rule = Schema.Struct({
321
+ id: Schema.String,
322
+ description: Schema.optionalKey(Schema.String),
323
+ /** ANDed conditions. An empty list always matches (useful as a catch-all). */
324
+ conditions: Schema.Array(Condition),
325
+ /** Reusable segments, ANDed with `conditions` (ADR-0006). */
326
+ segments: Schema.optionalKey(Schema.Array(SegmentRef)),
327
+ serve: Serve
328
+ });
329
+ /**
330
+ * A feature flag definition.
331
+ *
332
+ * Evaluation order: if `enabled` is false → serve `offVariant` (default
333
+ * `defaultVariant`) with reason `DISABLED`; otherwise walk `targeting` in
334
+ * order and serve the first matching rule (`TARGETING_MATCH`/`SPLIT`); if no
335
+ * rule matches, serve `defaultVariant` with reason `STATIC`.
336
+ */
337
+ const Flag = Schema.Struct({
338
+ key: Schema.String,
339
+ name: Schema.optionalKey(Schema.String),
340
+ description: Schema.optionalKey(Schema.String),
341
+ type: FlagType,
342
+ enabled: Schema.Boolean,
343
+ /** Named values; every served variant name must be a key here. */
344
+ variants: Schema.Record(Schema.String, FlagValue),
345
+ /** Variant served on a normal evaluation with no targeting match. */
346
+ defaultVariant: Schema.String,
347
+ /** Variant served when the flag is disabled. Defaults to `defaultVariant`. */
348
+ offVariant: Schema.optionalKey(Schema.String),
349
+ targeting: Schema.Array(Rule),
350
+ metadata: FlagMetadata,
351
+ createdAt: Schema.String,
352
+ updatedAt: Schema.String
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
+ });
383
+ /**
384
+ * Freeform organization labels for a flag (ADR-0008). A flag may carry any
385
+ * number; they group/filter flags in the dashboard and have **no** effect on
386
+ * evaluation. Deliberately separate from the spec `Flag` (and `metadata`) so
387
+ * they never reach the OFREP wire or the engine.
388
+ */
389
+ const Tags = Schema.Array(Schema.String.check(Schema.isMinLength(1)));
390
+ Schema.Struct({
391
+ ...Flag.fields,
392
+ tags: Tags
393
+ });
394
+ //#endregion
395
+ //#region ../schema/src/resolution.ts
396
+ /**
397
+ * Resolution reasons. The provider "SHOULD populate the resolution details
398
+ * structure's reason field with STATIC, DEFAULT, TARGETING_MATCH, SPLIT,
399
+ * CACHED, DISABLED, UNKNOWN, STALE, ERROR or some other string."
400
+ * (Requirement 2.2.5)
401
+ */
402
+ const Reason = Schema.Literals([
403
+ "STATIC",
404
+ "DEFAULT",
405
+ "TARGETING_MATCH",
406
+ "SPLIT",
407
+ "CACHED",
408
+ "DISABLED",
409
+ "UNKNOWN",
410
+ "STALE",
411
+ "ERROR"
412
+ ]);
413
+ /**
414
+ * OpenFeature error codes.
415
+ * @see https://openfeature.dev/specification/types
416
+ */
417
+ const ErrorCode = Schema.Literals([
418
+ "PROVIDER_NOT_READY",
419
+ "PROVIDER_FATAL",
420
+ "FLAG_NOT_FOUND",
421
+ "PARSE_ERROR",
422
+ "TYPE_MISMATCH",
423
+ "TARGETING_KEY_MISSING",
424
+ "INVALID_CONTEXT",
425
+ "GENERAL"
426
+ ]);
427
+ Schema.Struct({ context: Schema.optionalKey(EvaluationContext) });
428
+ /** A successful single/bulk evaluation result. */
429
+ const OfrepSuccess = Schema.Struct({
430
+ key: Schema.String,
431
+ value: FlagValue,
432
+ reason: Reason,
433
+ variant: Schema.optionalKey(Schema.String),
434
+ metadata: Schema.optionalKey(FlagMetadata)
435
+ });
436
+ /** A per-flag failure embedded in a bulk response. */
437
+ const OfrepFailure = Schema.Struct({
438
+ key: Schema.String,
439
+ errorCode: ErrorCode,
440
+ errorDetails: Schema.optionalKey(Schema.String),
441
+ metadata: Schema.optionalKey(FlagMetadata)
442
+ });
443
+ /** Bulk evaluation response: a mixed list of successes and failures. */
444
+ const OfrepBulkResponse = Schema.Struct({
445
+ flags: Schema.Array(Schema.Union([OfrepSuccess, OfrepFailure])),
446
+ metadata: Schema.optionalKey(FlagMetadata)
447
+ });
448
+ Schema.ErrorClass("OfrepFlagNotFound")({
449
+ key: Schema.String,
450
+ errorCode: Schema.Literal("FLAG_NOT_FOUND"),
451
+ errorDetails: Schema.optionalKey(Schema.String)
452
+ });
453
+ Schema.ErrorClass("OfrepEvaluationError")({
454
+ key: Schema.String,
455
+ errorCode: ErrorCode,
456
+ errorDetails: Schema.optionalKey(Schema.String)
457
+ });
458
+ Schema.Struct({
459
+ key: Schema.String.check(Schema.isMinLength(1)),
460
+ name: Schema.optionalKey(Schema.String),
461
+ description: Schema.optionalKey(Schema.String),
462
+ type: FlagType,
463
+ enabled: Schema.optionalKey(Schema.Boolean),
464
+ variants: Schema.Record(Schema.String, FlagValue),
465
+ defaultVariant: Schema.String,
466
+ offVariant: Schema.optionalKey(Schema.String),
467
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
468
+ metadata: Schema.optionalKey(FlagMetadata),
469
+ /** Freeform organization labels (ADR-0008). Normalized server-side. */
470
+ tags: Schema.optionalKey(Tags)
471
+ });
472
+ Schema.Struct({
473
+ name: Schema.optionalKey(Schema.String),
474
+ description: Schema.optionalKey(Schema.String),
475
+ type: FlagType,
476
+ enabled: Schema.optionalKey(Schema.Boolean),
477
+ variants: Schema.Record(Schema.String, FlagValue),
478
+ defaultVariant: Schema.String,
479
+ offVariant: Schema.optionalKey(Schema.String),
480
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
481
+ metadata: Schema.optionalKey(FlagMetadata),
482
+ tags: Schema.optionalKey(Tags)
483
+ });
484
+ Schema.Struct({
485
+ name: Schema.optionalKey(Schema.String),
486
+ description: Schema.optionalKey(Schema.String),
487
+ type: Schema.optionalKey(FlagType),
488
+ enabled: Schema.optionalKey(Schema.Boolean),
489
+ variants: Schema.optionalKey(Schema.Record(Schema.String, FlagValue)),
490
+ defaultVariant: Schema.optionalKey(Schema.String),
491
+ offVariant: Schema.optionalKey(Schema.String),
492
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
493
+ metadata: Schema.optionalKey(FlagMetadata),
494
+ tags: Schema.optionalKey(Tags)
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
+ });
539
+ //#endregion
540
+ //#region ../schema/src/sync.ts
541
+ /**
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.
546
+ *
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.
549
+ */
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
+ });
564
+ /**
565
+ * Every server→client sync-stream message. A single-member union today; the
566
+ * `type` discriminant is the extension point.
567
+ */
568
+ const SyncServerMessage = Schema.Union([SyncSnapshotMessage]);
569
+ Schema.Struct({ type: Schema.Literal("resync") });
570
+ //#endregion
571
+ export { OfrepSuccess as a, OfrepFailure as i, SyncServerMessage as n, OfrepBulkResponse as r, FlagSnapshot as t };
@@ -76,23 +76,49 @@ interface SyncOptions {
76
76
  * global `WebSocket` (Node ≥ 22). Useful for tests or proxies.
77
77
  */
78
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;
79
98
  }
80
99
  /** Configuration for a {@link https://toggld.io Toggld} client. */
81
100
  interface ToggldClientOptions {
82
101
  /**
83
102
  * An **evaluation** (or management) API key, sent as `Authorization: Bearer
84
103
  * <sdkKey>`. The key is scoped to one tenant + environment server-side, so a
85
- * client always evaluates against exactly that environment. Local evaluation
86
- * requires a secret key (`ffk_…`) — public browser tokens cannot read flag
87
- * definitions.
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.
88
111
  */
89
- readonly sdkKey: string;
112
+ readonly sdkKey?: string;
90
113
  /**
91
114
  * Base URL of the flag API, e.g. `https://toggld-api.example.workers.dev` or
92
115
  * `http://localhost:3000` in local development. No trailing path — the client
93
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.
94
120
  */
95
- readonly endpoint: string;
121
+ readonly endpoint?: string;
96
122
  /**
97
123
  * Evaluation mode. Defaults to `"remote"`; prefer `"local"` on long-lived
98
124
  * servers. See {@link EvaluationMode}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toggld/node",
3
- "version": "0.2.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
  ".": {