@toggld/node 0.2.0 → 0.4.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.
@@ -0,0 +1,638 @@
1
+ import { Schema } from "effect";
2
+ // ../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.update",
31
+ "environment.create",
32
+ "environment.delete",
33
+ "apiKey.create",
34
+ "apiKey.revoke",
35
+ "tenant.create",
36
+ "tenant.update",
37
+ "tenant.delete"
38
+ ]);
39
+ /** The kind of resource an event concerns; pairs with `resourceId`. */
40
+ const AuditResourceType = Schema.Literals([
41
+ "flag",
42
+ "segment",
43
+ "environment",
44
+ "apiKey",
45
+ "tenant"
46
+ ]);
47
+ Schema.Struct({
48
+ id: Schema.String,
49
+ /** Owning tenant; null only for control-plane events (tenant provisioning). */
50
+ tenantId: Schema.NullOr(Schema.String),
51
+ /** The environment touched; null for tenant-wide / definition-level / control-plane events. */
52
+ environmentId: Schema.NullOr(Schema.String),
53
+ actor: AuditActor,
54
+ action: AuditAction,
55
+ resourceType: AuditResourceType,
56
+ /** Flag key, environment id, API-key id, or tenant id. */
57
+ resourceId: Schema.String,
58
+ before: Schema.NullOr(Schema.Unknown),
59
+ after: Schema.NullOr(Schema.Unknown),
60
+ /** Non-authoritative request context (request id, source ip, …); empty in v0. */
61
+ metadata: Schema.Record(Schema.String, Schema.Unknown),
62
+ /** ISO 8601 timestamp the change was recorded. */
63
+ occurredAt: Schema.String
64
+ });
65
+ // ../schema/src/auth.ts
66
+ /**
67
+ * Authorization role carried by an API key.
68
+ *
69
+ * - `management` — full CRUD on the tenant's flags, plus evaluation.
70
+ * - `evaluation` — read-only evaluation (OFREP) of the tenant's flags.
71
+ */
72
+ const Role = Schema.Literals(["management", "evaluation"]);
73
+ Schema.Struct({
74
+ id: Schema.String,
75
+ name: Schema.String,
76
+ createdAt: Schema.String,
77
+ defaultEnvironmentId: Schema.optional(Schema.NullOr(Schema.String)),
78
+ /**
79
+ * Max number of flag *definitions* the tenant may hold; `null` = unlimited.
80
+ * The free tier is 10 (the DB column default). The flag API treats this as an
81
+ * opaque number — the web app owns the plan→limit mapping and pushes changes
82
+ * over the control plane (provision + PATCH). Enforced in `FlagService.create`.
83
+ */
84
+ flagLimit: Schema.NullOr(Schema.Int)
85
+ });
86
+ Schema.Struct({
87
+ id: Schema.String,
88
+ tenantId: Schema.String,
89
+ isProduction: Schema.optional(Schema.Boolean),
90
+ isDefault: Schema.optional(Schema.Boolean),
91
+ /** Key, unique within the tenant (e.g. `production`). */
92
+ key: Schema.String,
93
+ name: Schema.String,
94
+ createdAt: Schema.String
95
+ });
96
+ /** Body for creating an environment (POST /admin/v1/tenants/:tenantId/environments). */
97
+ const CreateEnvironmentInput = Schema.Struct({
98
+ isProduction: Schema.optional(Schema.Boolean),
99
+ makeDefault: Schema.optional(Schema.Boolean),
100
+ key: Schema.String.check(Schema.isMinLength(1)),
101
+ name: Schema.String.check(Schema.isMinLength(1))
102
+ });
103
+ Schema.Struct({
104
+ key: Schema.optional(Schema.String.check(Schema.isMinLength(1))),
105
+ name: Schema.optional(Schema.String.check(Schema.isMinLength(1))),
106
+ isProduction: Schema.optional(Schema.Boolean),
107
+ makeDefault: Schema.optional(Schema.Boolean)
108
+ });
109
+ Schema.Struct({
110
+ id: Schema.String,
111
+ tenantId: Schema.String,
112
+ /** The single environment this token is scoped to. */
113
+ environmentId: Schema.String,
114
+ /** The full token value — intentionally exposed; it is non-secret. */
115
+ token: Schema.String,
116
+ createdAt: Schema.String
117
+ });
118
+ /**
119
+ * Public view of an API key — safe to list. Never includes the secret token or
120
+ * its hash; only a non-secret prefix for human identification.
121
+ */
122
+ const ApiKeyInfo = Schema.Struct({
123
+ id: Schema.String,
124
+ tenantId: Schema.String,
125
+ /** The single environment this key is scoped to. */
126
+ environmentId: Schema.String,
127
+ name: Schema.optionalKey(Schema.String),
128
+ role: Role,
129
+ prefix: Schema.String,
130
+ createdAt: Schema.String
131
+ });
132
+ Schema.Struct({
133
+ ...ApiKeyInfo.fields,
134
+ token: Schema.String
135
+ });
136
+ Schema.Struct({
137
+ id: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),
138
+ name: Schema.String.check(Schema.isMinLength(1)),
139
+ environments: Schema.optionalKey(Schema.Array(CreateEnvironmentInput)),
140
+ /** Flag cap for the new tenant; omitted → the free-tier default (10). */
141
+ flagLimit: Schema.optionalKey(Schema.Int)
142
+ });
143
+ Schema.Struct({
144
+ name: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1))),
145
+ flagLimit: Schema.optionalKey(Schema.NullOr(Schema.Int))
146
+ });
147
+ Schema.Struct({
148
+ role: Role,
149
+ name: Schema.optionalKey(Schema.String),
150
+ environment: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1)))
151
+ });
152
+ // ../schema/src/value.ts
153
+ /**
154
+ * The OpenFeature flag value types. Every flag declares one of these; variant
155
+ * values and resolved values are validated against it.
156
+ *
157
+ * @see https://openfeature.dev/specification/types
158
+ */
159
+ const FlagType = Schema.Literals([
160
+ "boolean",
161
+ "string",
162
+ "number",
163
+ "object"
164
+ ]);
165
+ /**
166
+ * A resolved flag value. OpenFeature permits `boolean | string | number |
167
+ * structure`; we store the union as JSON, which also covers arrays and null
168
+ * for "object" typed flags.
169
+ */
170
+ const FlagValue = Schema.Json;
171
+ /**
172
+ * Flag metadata: "a structure supporting the definition of arbitrary
173
+ * properties, with keys of type string, and values of type boolean, string,
174
+ * or number." (Requirement 2.2.10)
175
+ */
176
+ const MetadataValue = Schema.Union([
177
+ Schema.String,
178
+ Schema.Finite,
179
+ Schema.Boolean
180
+ ]);
181
+ const FlagMetadata = Schema.Record(Schema.String, MetadataValue);
182
+ // ../schema/src/context.ts
183
+ /**
184
+ * An OpenFeature evaluation context: an optional `targetingKey` "identifying
185
+ * the subject of the flag evaluation" (Requirement 3.1.1) plus arbitrary
186
+ * custom fields (Requirement 3.1.2). We accept any JSON-valued attributes.
187
+ */
188
+ const EvaluationContext = Schema.Record(Schema.String, FlagValue);
189
+ Schema.Error("EnvironmentConflict")({
190
+ _tag: Schema.tag("EnvironmentConflict"),
191
+ message: Schema.String
192
+ });
193
+ Schema.Error("FlagNotFound")({
194
+ _tag: Schema.tag("FlagNotFound"),
195
+ flagKey: Schema.String,
196
+ message: Schema.String
197
+ });
198
+ Schema.Error("FlagConflict")({
199
+ _tag: Schema.tag("FlagConflict"),
200
+ flagKey: Schema.String,
201
+ message: Schema.String
202
+ });
203
+ Schema.Error("FlagAlreadyExists")({
204
+ _tag: Schema.tag("FlagAlreadyExists"),
205
+ flagKey: Schema.String,
206
+ message: Schema.String
207
+ });
208
+ Schema.Error("FlagValidationError")({
209
+ _tag: Schema.tag("FlagValidationError"),
210
+ flagKey: Schema.String,
211
+ issues: Schema.Array(Schema.String),
212
+ message: Schema.String
213
+ });
214
+ Schema.Error("FlagLimitExceeded")({
215
+ _tag: Schema.tag("FlagLimitExceeded"),
216
+ /** The tenant's current flag cap. */
217
+ limit: Schema.Int,
218
+ /** How many flags the tenant has now (>= limit). */
219
+ current: Schema.Int,
220
+ message: Schema.String
221
+ });
222
+ Schema.Error("Unauthorized")({
223
+ _tag: Schema.tag("Unauthorized"),
224
+ message: Schema.String
225
+ });
226
+ Schema.Error("Forbidden")({
227
+ _tag: Schema.tag("Forbidden"),
228
+ message: Schema.String
229
+ });
230
+ Schema.Error("TenantNotFound")({
231
+ _tag: Schema.tag("TenantNotFound"),
232
+ tenantId: Schema.String,
233
+ message: Schema.String
234
+ });
235
+ Schema.Error("TenantNotEmpty")({
236
+ _tag: Schema.tag("TenantNotEmpty"),
237
+ tenantId: Schema.String,
238
+ message: Schema.String
239
+ });
240
+ Schema.Error("TenantAlreadyExists")({
241
+ _tag: Schema.tag("TenantAlreadyExists"),
242
+ tenantId: Schema.String,
243
+ message: Schema.String
244
+ });
245
+ Schema.Error("EnvironmentNotFound")({
246
+ _tag: Schema.tag("EnvironmentNotFound"),
247
+ environment: Schema.String,
248
+ message: Schema.String
249
+ });
250
+ Schema.Error("EnvironmentAlreadyExists")({
251
+ _tag: Schema.tag("EnvironmentAlreadyExists"),
252
+ environment: Schema.String,
253
+ message: Schema.String
254
+ });
255
+ Schema.Error("ApiKeyNotFound")({
256
+ _tag: Schema.tag("ApiKeyNotFound"),
257
+ keyId: Schema.String,
258
+ message: Schema.String
259
+ });
260
+ Schema.Error("SegmentNotFound")({
261
+ _tag: Schema.tag("SegmentNotFound"),
262
+ segmentKey: Schema.String,
263
+ message: Schema.String
264
+ });
265
+ Schema.Error("SegmentAlreadyExists")({
266
+ _tag: Schema.tag("SegmentAlreadyExists"),
267
+ segmentKey: Schema.String,
268
+ message: Schema.String
269
+ });
270
+ Schema.Error("SegmentConflict")({
271
+ _tag: Schema.tag("SegmentConflict"),
272
+ segmentKey: Schema.String,
273
+ message: Schema.String
274
+ });
275
+ Schema.Error("SegmentValidationError")({
276
+ _tag: Schema.tag("SegmentValidationError"),
277
+ segmentKey: Schema.String,
278
+ issues: Schema.Array(Schema.String),
279
+ message: Schema.String
280
+ });
281
+ Schema.Error("SegmentReferenced")({
282
+ _tag: Schema.tag("SegmentReferenced"),
283
+ segmentKey: Schema.String,
284
+ /** Keys of the flags whose targeting still references the segment. */
285
+ flagKeys: Schema.Array(Schema.String),
286
+ message: Schema.String
287
+ });
288
+ // ../schema/src/flag.ts
289
+ /**
290
+ * Operators usable in a targeting condition. Comparisons coerce sensibly:
291
+ * numeric operators require numbers, string operators require strings, and
292
+ * `in`/`notIn` test membership in an array literal.
293
+ */
294
+ const Operator = Schema.Literals([
295
+ "eq",
296
+ "ne",
297
+ "in",
298
+ "notIn",
299
+ "lt",
300
+ "lte",
301
+ "gt",
302
+ "gte",
303
+ "contains",
304
+ "startsWith",
305
+ "endsWith",
306
+ "exists",
307
+ "notExists"
308
+ ]);
309
+ /**
310
+ * A single targeting condition tested against an attribute of the evaluation
311
+ * context (e.g. `targetingKey`, `email`, `plan`). All conditions in a rule are
312
+ * ANDed together.
313
+ */
314
+ const Condition = Schema.Struct({
315
+ attribute: Schema.String,
316
+ operator: Operator,
317
+ /** Comparison operand. Ignored for `exists`/`notExists`. */
318
+ value: Schema.optionalKey(FlagValue)
319
+ });
320
+ /** A weighted slice of a percentage rollout. */
321
+ const Split = Schema.Struct({
322
+ variant: Schema.String,
323
+ /** Relative weight; the engine normalises weights across all splits. */
324
+ weight: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
325
+ });
326
+ /** Serve a single fixed variant when the rule matches → `TARGETING_MATCH`. */
327
+ const ServeVariant = Schema.Struct({
328
+ type: Schema.Literal("variant"),
329
+ variant: Schema.String
330
+ });
331
+ /**
332
+ * Serve a deterministic percentage rollout when the rule matches → `SPLIT`.
333
+ * Bucketing is consistent for a given subject (`bucketBy`, default
334
+ * `targetingKey`) so a subject always lands in the same slice.
335
+ */
336
+ const ServeRollout = Schema.Struct({
337
+ type: Schema.Literal("rollout"),
338
+ bucketBy: Schema.optionalKey(Schema.String),
339
+ splits: Schema.Array(Split)
340
+ });
341
+ const Serve = Schema.Union([ServeVariant, ServeRollout]);
342
+ /** A reference to a reusable {@link Segment} from a targeting rule (ADR-0006). */
343
+ const SegmentRef = Schema.Struct({
344
+ segment: Schema.String,
345
+ negate: Schema.optionalKey(Schema.Boolean)
346
+ });
347
+ /** An ordered targeting rule. The first rule whose conditions all match wins. */
348
+ const Rule = Schema.Struct({
349
+ id: Schema.String,
350
+ description: Schema.optionalKey(Schema.String),
351
+ /** ANDed conditions. An empty list always matches (useful as a catch-all). */
352
+ conditions: Schema.Array(Condition),
353
+ /** Reusable segments, ANDed with `conditions` (ADR-0006). */
354
+ segments: Schema.optionalKey(Schema.Array(SegmentRef)),
355
+ serve: Serve
356
+ });
357
+ /**
358
+ * A feature flag definition.
359
+ *
360
+ * Evaluation order: if `enabled` is false → serve `offVariant` (default
361
+ * `defaultVariant`) with reason `DISABLED`; otherwise walk `targeting` in
362
+ * order and serve the first matching rule (`TARGETING_MATCH`/`SPLIT`); if no
363
+ * rule matches, serve `defaultVariant` with reason `STATIC`.
364
+ */
365
+ const Flag = Schema.Struct({
366
+ key: Schema.String,
367
+ name: Schema.optionalKey(Schema.String),
368
+ description: Schema.optionalKey(Schema.String),
369
+ type: FlagType,
370
+ enabled: Schema.Boolean,
371
+ /** Named values; every served variant name must be a key here. */
372
+ variants: Schema.Record(Schema.String, FlagValue),
373
+ /** Variant served on a normal evaluation with no targeting match. */
374
+ defaultVariant: Schema.String,
375
+ /** Variant served when the flag is disabled. Defaults to `defaultVariant`. */
376
+ offVariant: Schema.optionalKey(Schema.String),
377
+ targeting: Schema.Array(Rule),
378
+ metadata: FlagMetadata,
379
+ createdAt: Schema.String,
380
+ updatedAt: Schema.String
381
+ });
382
+ /** How a segment's enumerated member list is distributed for evaluation. */
383
+ const SegmentListMode = Schema.Literals([
384
+ "none",
385
+ "inline",
386
+ "remote"
387
+ ]);
388
+ /** A single segment membership rule: ANDed conditions, no `serve` (ADR-0006). */
389
+ const SegmentRule = Schema.Struct({
390
+ id: Schema.String,
391
+ description: Schema.optionalKey(Schema.String),
392
+ conditions: Schema.Array(Condition)
393
+ });
394
+ /**
395
+ * A reusable audience definition as it rides the sync bundle (ADR-0006). Inline
396
+ * member keys are present only for `inline` segments; `remote` segments omit
397
+ * them and resolve via a backing-store lookup.
398
+ */
399
+ const Segment = Schema.Struct({
400
+ key: Schema.String,
401
+ name: Schema.optionalKey(Schema.String),
402
+ description: Schema.optionalKey(Schema.String),
403
+ keyAttribute: Schema.String,
404
+ rules: Schema.Array(SegmentRule),
405
+ listMode: SegmentListMode,
406
+ members: Schema.optionalKey(Schema.Array(Schema.String)),
407
+ version: Schema.Number,
408
+ createdAt: Schema.String,
409
+ updatedAt: Schema.String
410
+ });
411
+ /**
412
+ * Freeform organization labels for a flag (ADR-0008). A flag may carry any
413
+ * number; they group/filter flags in the dashboard and have **no** effect on
414
+ * evaluation. Deliberately separate from the spec `Flag` (and `metadata`) so
415
+ * they never reach the OFREP wire or the engine.
416
+ */
417
+ const Tags = Schema.Array(Schema.String.check(Schema.isMinLength(1)));
418
+ Schema.Struct({
419
+ ...Flag.fields,
420
+ tags: Tags,
421
+ version: Schema.String
422
+ });
423
+ // ../schema/src/resolution.ts
424
+ /**
425
+ * Resolution reasons. The provider "SHOULD populate the resolution details
426
+ * structure's reason field with STATIC, DEFAULT, TARGETING_MATCH, SPLIT,
427
+ * CACHED, DISABLED, UNKNOWN, STALE, ERROR or some other string."
428
+ * (Requirement 2.2.5)
429
+ */
430
+ const Reason = Schema.Literals([
431
+ "STATIC",
432
+ "DEFAULT",
433
+ "TARGETING_MATCH",
434
+ "SPLIT",
435
+ "CACHED",
436
+ "DISABLED",
437
+ "UNKNOWN",
438
+ "STALE",
439
+ "ERROR"
440
+ ]);
441
+ /**
442
+ * OpenFeature error codes.
443
+ * @see https://openfeature.dev/specification/types
444
+ */
445
+ const ErrorCode = Schema.Literals([
446
+ "PROVIDER_NOT_READY",
447
+ "PROVIDER_FATAL",
448
+ "FLAG_NOT_FOUND",
449
+ "PARSE_ERROR",
450
+ "TYPE_MISMATCH",
451
+ "TARGETING_KEY_MISSING",
452
+ "INVALID_CONTEXT",
453
+ "GENERAL"
454
+ ]);
455
+ Schema.Struct({ context: Schema.optionalKey(EvaluationContext) });
456
+ /** A successful single/bulk evaluation result. */
457
+ const OfrepSuccess = Schema.Struct({
458
+ key: Schema.String,
459
+ value: FlagValue,
460
+ reason: Reason,
461
+ variant: Schema.optionalKey(Schema.String),
462
+ metadata: Schema.optionalKey(FlagMetadata)
463
+ });
464
+ /** A per-flag failure embedded in a bulk response. */
465
+ const OfrepFailure = Schema.Struct({
466
+ key: Schema.String,
467
+ errorCode: ErrorCode,
468
+ errorDetails: Schema.optionalKey(Schema.String),
469
+ metadata: Schema.optionalKey(FlagMetadata)
470
+ });
471
+ /** Bulk evaluation response: a mixed list of successes and failures. */
472
+ const OfrepBulkResponse = Schema.Struct({
473
+ flags: Schema.Array(Schema.Union([OfrepSuccess, OfrepFailure])),
474
+ metadata: Schema.optionalKey(FlagMetadata)
475
+ });
476
+ Schema.Error("OfrepFlagNotFound")({
477
+ key: Schema.String,
478
+ errorCode: Schema.Literal("FLAG_NOT_FOUND"),
479
+ errorDetails: Schema.optionalKey(Schema.String)
480
+ });
481
+ Schema.Error("OfrepEvaluationError")({
482
+ key: Schema.String,
483
+ errorCode: ErrorCode,
484
+ errorDetails: Schema.optionalKey(Schema.String)
485
+ });
486
+ Schema.Struct({
487
+ key: Schema.String.check(Schema.isMinLength(1)),
488
+ name: Schema.optionalKey(Schema.String),
489
+ description: Schema.optionalKey(Schema.String),
490
+ type: FlagType,
491
+ enabled: Schema.optionalKey(Schema.Boolean),
492
+ variants: Schema.Record(Schema.String, FlagValue),
493
+ defaultVariant: Schema.String,
494
+ offVariant: Schema.optionalKey(Schema.String),
495
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
496
+ metadata: Schema.optionalKey(FlagMetadata),
497
+ /** Freeform organization labels (ADR-0008). Normalized server-side. */
498
+ tags: Schema.optionalKey(Tags)
499
+ });
500
+ Schema.Struct({
501
+ expectedVersion: Schema.optionalKey(Schema.String),
502
+ name: Schema.optionalKey(Schema.String),
503
+ description: Schema.optionalKey(Schema.String),
504
+ type: FlagType,
505
+ enabled: Schema.optionalKey(Schema.Boolean),
506
+ variants: Schema.Record(Schema.String, FlagValue),
507
+ defaultVariant: Schema.String,
508
+ offVariant: Schema.optionalKey(Schema.String),
509
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
510
+ metadata: Schema.optionalKey(FlagMetadata),
511
+ tags: Schema.optionalKey(Tags)
512
+ });
513
+ Schema.Struct({
514
+ expectedVersion: Schema.optionalKey(Schema.String),
515
+ name: Schema.optionalKey(Schema.String),
516
+ description: Schema.optionalKey(Schema.String),
517
+ type: Schema.optionalKey(FlagType),
518
+ enabled: Schema.optionalKey(Schema.Boolean),
519
+ variants: Schema.optionalKey(Schema.Record(Schema.String, FlagValue)),
520
+ defaultVariant: Schema.optionalKey(Schema.String),
521
+ offVariant: Schema.optionalKey(Schema.String),
522
+ targeting: Schema.optionalKey(Schema.Array(Rule)),
523
+ metadata: Schema.optionalKey(FlagMetadata),
524
+ tags: Schema.optionalKey(Tags)
525
+ });
526
+ Schema.Struct({
527
+ key: Schema.String,
528
+ name: Schema.optionalKey(Schema.String),
529
+ description: Schema.optionalKey(Schema.String),
530
+ keyAttribute: Schema.String,
531
+ rules: Schema.Array(SegmentRule),
532
+ listMode: Segment.fields.listMode,
533
+ /** Number of enumerated members in the list (0 for a rules-only segment). */
534
+ memberCount: Schema.Number,
535
+ version: Schema.Number,
536
+ /** Keys of the flags whose targeting references this segment, in any environment. */
537
+ usedBy: Schema.Array(Schema.String),
538
+ createdAt: Schema.String,
539
+ updatedAt: Schema.String
540
+ });
541
+ Schema.Struct({
542
+ key: Schema.String.check(Schema.isMinLength(1)),
543
+ name: Schema.optionalKey(Schema.String),
544
+ description: Schema.optionalKey(Schema.String),
545
+ /** Defaults to `targetingKey` server-side when omitted. */
546
+ keyAttribute: Schema.optionalKey(Schema.String),
547
+ rules: Schema.optionalKey(Schema.Array(SegmentRule))
548
+ });
549
+ Schema.Struct({
550
+ /** Reject a stale definition draft when supplied. */
551
+ expectedVersion: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
552
+ name: Schema.optionalKey(Schema.String),
553
+ description: Schema.optionalKey(Schema.String),
554
+ keyAttribute: Schema.optionalKey(Schema.String),
555
+ rules: Schema.optionalKey(Schema.Array(SegmentRule))
556
+ });
557
+ Schema.Struct({
558
+ add: Schema.optionalKey(Schema.Array(Schema.String)),
559
+ remove: Schema.optionalKey(Schema.Array(Schema.String))
560
+ });
561
+ /** One enumerated member of a segment's list. */
562
+ const SegmentMember = Schema.Struct({
563
+ key: Schema.String,
564
+ addedAt: Schema.String
565
+ });
566
+ Schema.Struct({
567
+ members: Schema.Array(SegmentMember),
568
+ /** Total members matching the (optional) query — the count line in the UI. */
569
+ total: Schema.Number
570
+ });
571
+ // ../schema/src/sync.ts
572
+ /**
573
+ * Definition-sync wire schemas (ADR-0001 §4): the versioned full snapshot the
574
+ * sync surface serves to locally-evaluating SDKs, and the messages exchanged on
575
+ * the live sync stream. Like the OFREP schemas, these are shared by the server
576
+ * (producer) and `@toggld/node` (consumer) so the contract cannot drift.
577
+ *
578
+ * `version` is opaque and equality-compared (ADR-0005 phase 1); the ordered
579
+ * `config_version` lands in this same field later without a wire change.
580
+ */
581
+ /** A versioned full snapshot of one environment's resolved flag set. */
582
+ const FlagSnapshot = Schema.Struct({
583
+ version: Schema.String,
584
+ flags: Schema.Array(Flag),
585
+ /** Segment definitions referenced by the flags (ADR-0006). */
586
+ segments: Schema.optionalKey(Schema.Array(Segment))
587
+ });
588
+ /** Server→client: the current snapshot (connect greeting and change pushes). */
589
+ const SyncSnapshotMessage = Schema.Struct({
590
+ type: Schema.Literal("snapshot"),
591
+ version: Schema.String,
592
+ flags: Schema.Array(Flag),
593
+ segments: Schema.optionalKey(Schema.Array(Segment))
594
+ });
595
+ /**
596
+ * Every server→client sync-stream message. A single-member union today; the
597
+ * `type` discriminant is the extension point.
598
+ */
599
+ const SyncServerMessage = Schema.Union([SyncSnapshotMessage]);
600
+ Schema.Struct({ type: Schema.Literal("resync") });
601
+ // ../schema/src/telemetry.ts
602
+ const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
603
+ const EvaluationRollup = Schema.Struct({
604
+ flagKey: Identifier,
605
+ variant: Schema.String.check(Schema.isMaxLength(256)),
606
+ reason: Reason,
607
+ bucketStart: Schema.String.check(Schema.makeFilter((s) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\.000Z$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString() === s)),
608
+ count: Schema.Number.check(Schema.isInt(), Schema.isBetween({
609
+ minimum: 1,
610
+ maximum: 1e9
611
+ }))
612
+ });
613
+ Schema.Struct({
614
+ sdkInstanceId: Identifier,
615
+ windowId: Identifier,
616
+ rollups: Schema.Array(EvaluationRollup).check(Schema.isMinLength(1), Schema.isMaxLength(1e3))
617
+ });
618
+ Schema.Struct({
619
+ range: Schema.Literals([
620
+ "1h",
621
+ "24h",
622
+ "7d"
623
+ ]),
624
+ from: Schema.String,
625
+ to: Schema.String,
626
+ total: Schema.Number,
627
+ errors: Schema.Number,
628
+ lastEvaluatedAt: Schema.NullOr(Schema.String),
629
+ timeline: Schema.Array(Schema.Struct({
630
+ bucketStart: Schema.String,
631
+ count: Schema.Number
632
+ })),
633
+ variants: Schema.Array(Schema.Struct({
634
+ variant: Schema.String,
635
+ count: Schema.Number
636
+ }))
637
+ });
638
+ export { OfrepSuccess as a, OfrepFailure as i, SyncServerMessage as n, OfrepBulkResponse as r, FlagSnapshot as t };