@smplkit/sdk 1.3.11 → 1.3.13

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.cjs CHANGED
@@ -30,33 +30,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- BoolFlagHandle: () => BoolFlagHandle,
33
+ BooleanFlag: () => BooleanFlag,
34
34
  Config: () => Config,
35
35
  ConfigClient: () => ConfigClient,
36
36
  Context: () => Context,
37
- ContextType: () => ContextType,
38
37
  Flag: () => Flag,
39
38
  FlagChangeEvent: () => FlagChangeEvent,
40
39
  FlagStats: () => FlagStats,
41
40
  FlagsClient: () => FlagsClient,
42
- JsonFlagHandle: () => JsonFlagHandle,
43
- NumberFlagHandle: () => NumberFlagHandle,
41
+ JsonFlag: () => JsonFlag,
42
+ LiveConfigProxy: () => LiveConfigProxy,
43
+ LogGroup: () => LogGroup,
44
+ LogLevel: () => LogLevel,
45
+ Logger: () => Logger,
46
+ LoggingClient: () => LoggingClient,
47
+ NumberFlag: () => NumberFlag,
44
48
  Rule: () => Rule,
45
49
  SharedWebSocket: () => SharedWebSocket,
46
50
  SmplClient: () => SmplClient,
47
51
  SmplConflictError: () => SmplConflictError,
48
52
  SmplConnectionError: () => SmplConnectionError,
49
53
  SmplError: () => SmplError,
50
- SmplNotConnectedError: () => SmplNotConnectedError,
51
54
  SmplNotFoundError: () => SmplNotFoundError,
52
55
  SmplTimeoutError: () => SmplTimeoutError,
53
56
  SmplValidationError: () => SmplValidationError,
54
- StringFlagHandle: () => StringFlagHandle
57
+ StringFlag: () => StringFlag
55
58
  });
56
59
  module.exports = __toCommonJS(index_exports);
57
60
 
58
61
  // src/client.ts
59
- var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
62
+ var import_openapi_fetch4 = __toESM(require("openapi-fetch"), 1);
60
63
 
61
64
  // src/config/client.ts
62
65
  var import_openapi_fetch = __toESM(require("openapi-fetch"), 1);
@@ -119,13 +122,6 @@ var SmplConflictError = class extends SmplError {
119
122
  Object.setPrototypeOf(this, new.target.prototype);
120
123
  }
121
124
  };
122
- var SmplNotConnectedError = class extends SmplError {
123
- constructor(message) {
124
- super(message);
125
- this.name = "SmplNotConnectedError";
126
- Object.setPrototypeOf(this, new.target.prototype);
127
- }
128
- };
129
125
  var SmplValidationError = class extends SmplError {
130
126
  constructor(message, statusCode, responseBody, errors) {
131
127
  super(message, statusCode ?? 422, responseBody, errors);
@@ -205,9 +201,9 @@ function resolveChain(chain, environment) {
205
201
 
206
202
  // src/config/types.ts
207
203
  var Config = class {
208
- /** UUID of the config. */
204
+ /** UUID of the config, or `null` if unsaved. */
209
205
  id;
210
- /** Human-readable key (e.g. `"user_service"`). */
206
+ /** Human-readable key (e.g. `"user-service"`). */
211
207
  key;
212
208
  /** Display name. */
213
209
  name;
@@ -227,10 +223,7 @@ var Config = class {
227
223
  createdAt;
228
224
  /** When the config was last updated, or null if unavailable. */
229
225
  updatedAt;
230
- /**
231
- * Internal reference to the parent client.
232
- * @internal
233
- */
226
+ /** @internal */
234
227
  _client;
235
228
  /** @internal */
236
229
  constructor(client, fields) {
@@ -246,100 +239,31 @@ var Config = class {
246
239
  this.updatedAt = fields.updatedAt;
247
240
  }
248
241
  /**
249
- * Update this config's attributes on the server.
250
- *
251
- * Builds the request from current attribute values, overriding with any
252
- * provided options. Updates local attributes in place on success.
253
- *
254
- * @param options.name - New display name.
255
- * @param options.description - New description (pass empty string to clear).
256
- * @param options.items - New base values (replaces entirely).
257
- * @param options.environments - New environments dict (replaces entirely).
258
- */
259
- async update(options) {
260
- const updated = await this._client._updateConfig({
261
- configId: this.id,
262
- name: options.name ?? this.name,
263
- key: this.key,
264
- description: options.description !== void 0 ? options.description : this.description,
265
- parent: this.parent,
266
- items: options.items ?? this.items,
267
- environments: options.environments ?? this.environments
268
- });
269
- this.name = updated.name;
270
- this.description = updated.description;
271
- this.items = updated.items;
272
- this.environments = updated.environments;
273
- this.updatedAt = updated.updatedAt;
274
- }
275
- /**
276
- * Replace base or environment-specific values.
277
- *
278
- * When `environment` is provided, replaces that environment's `values`
279
- * sub-dict (other environments are preserved). When omitted, replaces
280
- * the base `items`.
281
- *
282
- * @param values - The complete set of values to set.
283
- * @param environment - Target environment, or omit for base values.
284
- */
285
- async setValues(values, environment) {
286
- let newItems;
287
- let newEnvs;
288
- if (environment === void 0) {
289
- newItems = values;
290
- newEnvs = this.environments;
291
- } else {
292
- newItems = this.items;
293
- const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
294
- existingEntry.values = values;
295
- newEnvs = { ...this.environments, [environment]: existingEntry };
296
- }
297
- const updated = await this._client._updateConfig({
298
- configId: this.id,
299
- name: this.name,
300
- key: this.key,
301
- description: this.description,
302
- parent: this.parent,
303
- items: newItems,
304
- environments: newEnvs
305
- });
306
- this.items = updated.items;
307
- this.environments = updated.environments;
308
- this.updatedAt = updated.updatedAt;
309
- }
310
- /**
311
- * Set a single key within base or environment-specific values.
242
+ * Persist this config to the server.
312
243
  *
313
- * Merges the key into existing values rather than replacing all values.
314
- *
315
- * @param key - The config key to set.
316
- * @param value - The value to assign.
317
- * @param environment - Target environment, or omit for base values.
244
+ * POST if `id` is null (new config), PUT if `id` is set (update).
245
+ * Updates this instance in-place with the server response.
318
246
  */
319
- async setValue(key, value, environment) {
320
- if (environment === void 0) {
321
- const merged = { ...this.items, [key]: value };
322
- await this.setValues(merged);
247
+ async save() {
248
+ if (this.id === null) {
249
+ const created = await this._client._createConfig(this);
250
+ this._apply(created);
323
251
  } else {
324
- const envEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? this.environments[environment] : {};
325
- const existing = {
326
- ...typeof envEntry.values === "object" && envEntry.values !== null ? envEntry.values : {}
327
- };
328
- existing[key] = value;
329
- await this.setValues(existing, environment);
252
+ const updated = await this._client._updateConfig(this);
253
+ this._apply(updated);
330
254
  }
331
255
  }
332
256
  /**
333
257
  * Walk the parent chain and return config data objects, child-to-root.
334
258
  * @internal
335
259
  */
336
- async _buildChain(_timeout) {
337
- const chain = [{ id: this.id, items: this.items, environments: this.environments }];
260
+ async _buildChain() {
261
+ const chain = [{ id: this.id ?? "", items: this.items, environments: this.environments }];
338
262
  let parentId = this.parent;
339
263
  while (parentId !== null) {
340
- const parentConfig = await this._client.get({ id: parentId });
264
+ const parentConfig = await this._client._getById(parentId);
341
265
  chain.push({
342
- id: parentConfig.id,
266
+ id: parentConfig.id ?? "",
343
267
  items: parentConfig.items,
344
268
  environments: parentConfig.environments
345
269
  });
@@ -347,11 +271,82 @@ var Config = class {
347
271
  }
348
272
  return chain;
349
273
  }
274
+ /** @internal — copy all fields from another Config instance. */
275
+ _apply(other) {
276
+ this.id = other.id;
277
+ this.key = other.key;
278
+ this.name = other.name;
279
+ this.description = other.description;
280
+ this.parent = other.parent;
281
+ this.items = other.items;
282
+ this.environments = other.environments;
283
+ this.createdAt = other.createdAt;
284
+ this.updatedAt = other.updatedAt;
285
+ }
350
286
  toString() {
351
287
  return `Config(id=${this.id}, key=${this.key}, name=${this.name})`;
352
288
  }
353
289
  };
354
290
 
291
+ // src/config/proxy.ts
292
+ var LiveConfigProxy = class {
293
+ /** @internal */
294
+ _client;
295
+ /** @internal */
296
+ _key;
297
+ /** @internal */
298
+ _model;
299
+ constructor(client, key, model) {
300
+ this._client = client;
301
+ this._key = key;
302
+ this._model = model;
303
+ return new Proxy(this, {
304
+ get(target, prop, receiver) {
305
+ if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
306
+ return Reflect.get(target, prop, receiver);
307
+ }
308
+ const values = target._currentValues();
309
+ if (target._model) {
310
+ const instance = new target._model(values);
311
+ return instance[prop];
312
+ }
313
+ return values[prop];
314
+ },
315
+ has(target, prop) {
316
+ if (typeof prop === "symbol") return Reflect.has(target, prop);
317
+ const values = target._currentValues();
318
+ return prop in values;
319
+ },
320
+ ownKeys(target) {
321
+ const values = target._currentValues();
322
+ return Object.keys(values);
323
+ },
324
+ getOwnPropertyDescriptor(target, prop) {
325
+ if (typeof prop === "symbol") return Reflect.getOwnPropertyDescriptor(target, prop);
326
+ const values = target._currentValues();
327
+ if (prop in values) {
328
+ return {
329
+ configurable: true,
330
+ enumerable: true,
331
+ value: values[prop],
332
+ writable: false
333
+ };
334
+ }
335
+ return void 0;
336
+ }
337
+ });
338
+ }
339
+ /** @internal */
340
+ _currentValues() {
341
+ return this._client._getCachedConfig(this._key) ?? {};
342
+ }
343
+ };
344
+
345
+ // src/helpers.ts
346
+ function keyToDisplayName(key) {
347
+ return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
348
+ }
349
+
355
350
  // src/config/client.ts
356
351
  var BASE_URL = "https://config.smplkit.com";
357
352
  function extractItemValues(items) {
@@ -381,7 +376,7 @@ function extractEnvironments(environments) {
381
376
  function resourceToConfig(resource, client) {
382
377
  const attrs = resource.attributes;
383
378
  return new Config(client, {
384
- id: resource.id ?? "",
379
+ id: resource.id ?? null,
385
380
  key: attrs.key ?? "",
386
381
  name: attrs.name,
387
382
  description: attrs.description ?? null,
@@ -390,8 +385,8 @@ function resourceToConfig(resource, client) {
390
385
  environments: extractEnvironments(
391
386
  attrs.environments
392
387
  ),
393
- createdAt: attrs.created_at ? new Date(attrs.created_at) : null,
394
- updatedAt: attrs.updated_at ? new Date(attrs.updated_at) : null
388
+ createdAt: attrs.created_at ?? null,
389
+ updatedAt: attrs.updated_at ?? null
395
390
  });
396
391
  }
397
392
  async function checkError(response, _context) {
@@ -458,7 +453,7 @@ function buildRequestBody(options) {
458
453
  };
459
454
  }
460
455
  var ConfigClient = class {
461
- /** @internal — used by Config instances for reconnecting and WebSocket auth. */
456
+ /** @internal */
462
457
  _apiKey;
463
458
  /** @internal */
464
459
  _baseUrl = BASE_URL;
@@ -469,7 +464,9 @@ var ConfigClient = class {
469
464
  /** @internal — set by SmplClient after construction. */
470
465
  _parent = null;
471
466
  _configCache = {};
472
- _connected = false;
467
+ /* v8 ignore next — bookkeeping for future use */
468
+ _configStore = [];
469
+ _initialized = false;
473
470
  _listeners = [];
474
471
  /** @internal */
475
472
  constructor(apiKey, timeout) {
@@ -481,7 +478,6 @@ var ConfigClient = class {
481
478
  Authorization: `Bearer ${apiKey}`,
482
479
  Accept: "application/json"
483
480
  },
484
- // openapi-fetch custom fetch receives a pre-built Request object
485
481
  fetch: async (request) => {
486
482
  const controller = new AbortController();
487
483
  const timer = setTimeout(() => controller.abort(), ms);
@@ -498,23 +494,31 @@ var ConfigClient = class {
498
494
  }
499
495
  });
500
496
  }
501
- /**
502
- * Fetch a single config by key or UUID.
503
- *
504
- * Exactly one of `key` or `id` must be provided.
505
- *
506
- * @throws {SmplNotFoundError} If no matching config exists.
507
- */
508
- async get(options) {
509
- const { key, id } = options;
510
- if (key === void 0 === (id === void 0)) {
511
- throw new Error("Exactly one of 'key' or 'id' must be provided.");
512
- }
513
- return id !== void 0 ? this._getById(id) : this._getByKey(key);
497
+ // ------------------------------------------------------------------
498
+ // Management: factory method
499
+ // ------------------------------------------------------------------
500
+ /** Create an unsaved config. Call `.save()` to persist. */
501
+ new(key, options) {
502
+ return new Config(this, {
503
+ id: null,
504
+ key,
505
+ name: options?.name ?? keyToDisplayName(key),
506
+ description: options?.description ?? null,
507
+ parent: options?.parent ?? null,
508
+ items: {},
509
+ environments: {},
510
+ createdAt: null,
511
+ updatedAt: null
512
+ });
514
513
  }
515
- /**
516
- * List all configs for the account.
517
- */
514
+ // ------------------------------------------------------------------
515
+ // Management: CRUD
516
+ // ------------------------------------------------------------------
517
+ /** Fetch a config by key. */
518
+ async get(key) {
519
+ return this._getByKey(key);
520
+ }
521
+ /** List all configs. */
518
522
  async list() {
519
523
  let data;
520
524
  try {
@@ -527,18 +531,31 @@ var ConfigClient = class {
527
531
  if (!data) return [];
528
532
  return data.data.map((r) => resourceToConfig(r, this));
529
533
  }
530
- /**
531
- * Create a new config.
532
- *
533
- * @throws {SmplValidationError} If the server rejects the request.
534
- */
535
- async create(options) {
534
+ /** Delete a config by key. */
535
+ async delete(key) {
536
+ const config = await this.get(key);
537
+ try {
538
+ const result = await this._http.DELETE("/api/v1/configs/{id}", {
539
+ params: { path: { id: config.id } }
540
+ });
541
+ if (result.error !== void 0 && result.response.status !== 204)
542
+ await checkError(result.response, `Failed to delete config '${key}'`);
543
+ } catch (err) {
544
+ wrapFetchError(err);
545
+ }
546
+ }
547
+ // ------------------------------------------------------------------
548
+ // Management: internal save methods (called by Config.save())
549
+ // ------------------------------------------------------------------
550
+ /** @internal — POST a new config. */
551
+ async _createConfig(config) {
536
552
  const body = buildRequestBody({
537
- name: options.name,
538
- key: options.key,
539
- description: options.description,
540
- parent: options.parent,
541
- items: options.items
553
+ name: config.name,
554
+ key: config.key,
555
+ description: config.description,
556
+ parent: config.parent,
557
+ items: config.items,
558
+ environments: config.environments
542
559
  });
543
560
  let data;
544
561
  try {
@@ -551,127 +568,191 @@ var ConfigClient = class {
551
568
  if (!data || !data.data) throw new SmplValidationError("Failed to create config");
552
569
  return resourceToConfig(data.data, this);
553
570
  }
554
- /**
555
- * Delete a config by UUID.
556
- *
557
- * @throws {SmplNotFoundError} If the config does not exist.
558
- * @throws {SmplConflictError} If the config has child configs.
559
- */
560
- async delete(configId) {
571
+ /** @internal — PUT a config update. */
572
+ async _updateConfig(config) {
573
+ const body = buildRequestBody({
574
+ id: config.id,
575
+ name: config.name,
576
+ key: config.key,
577
+ description: config.description,
578
+ parent: config.parent,
579
+ items: config.items,
580
+ environments: config.environments
581
+ });
582
+ let data;
561
583
  try {
562
- const result = await this._http.DELETE("/api/v1/configs/{id}", {
563
- params: { path: { id: configId } }
584
+ const result = await this._http.PUT("/api/v1/configs/{id}", {
585
+ params: { path: { id: config.id } },
586
+ body
564
587
  });
565
- if (result.error !== void 0 && result.response.status !== 204)
566
- await checkError(result.response, `Failed to delete config ${configId}`);
588
+ if (result.error !== void 0)
589
+ await checkError(result.response, `Failed to update config ${config.id}`);
590
+ data = result.data;
567
591
  } catch (err) {
568
592
  wrapFetchError(err);
569
593
  }
594
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
595
+ return resourceToConfig(data.data, this);
570
596
  }
571
- /**
572
- * Fetch all configs, resolve values for the environment, and cache.
573
- * @internal — called by SmplClient.connect().
574
- */
575
- async _connectInternal(environment) {
576
- const configs = await this.list();
577
- const cache = {};
578
- for (const cfg of configs) {
579
- const chain = await cfg._buildChain(this._http);
580
- cache[cfg.key] = resolveChain(chain, environment);
597
+ /** @internal — fetch a config by UUID. */
598
+ async _getById(configId) {
599
+ let data;
600
+ try {
601
+ const result = await this._http.GET("/api/v1/configs/{id}", {
602
+ params: { path: { id: configId } }
603
+ });
604
+ if (result.error !== void 0)
605
+ await checkError(result.response, `Config ${configId} not found`);
606
+ data = result.data;
607
+ } catch (err) {
608
+ wrapFetchError(err);
581
609
  }
582
- this._configCache = cache;
583
- this._connected = true;
610
+ if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
611
+ return resourceToConfig(data.data, this);
584
612
  }
613
+ // ------------------------------------------------------------------
614
+ // Runtime: resolve and subscribe
615
+ // ------------------------------------------------------------------
585
616
  /**
586
- * Read a resolved config value (prescriptive access).
617
+ * Resolve a config's values for the current environment.
587
618
  *
588
- * Requires {@link SmplClient.connect} to have been called.
619
+ * Returns a flat dict of resolved key-value pairs, walking the
620
+ * parent chain and applying environment overrides.
589
621
  *
590
- * @param configKey - The config key to look up.
591
- * @param itemKey - Optional specific item key. If omitted, returns all values.
592
- * @param defaultValue - Default value if the key is missing.
593
- *
594
- * @throws {SmplNotConnectedError} If connect() has not been called.
622
+ * Optionally pass a model class to map the resolved values.
595
623
  */
596
- getValue(configKey, itemKey, defaultValue) {
597
- if (!this._connected) {
598
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
599
- }
600
- const resolved = this._configCache[configKey];
601
- if (resolved === void 0) {
602
- return defaultValue ?? null;
624
+ async resolve(key, model) {
625
+ await this._ensureInitialized();
626
+ const values = this._configCache[key];
627
+ if (values === void 0) {
628
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
603
629
  }
604
- if (itemKey === void 0) {
605
- return { ...resolved };
630
+ if (model) {
631
+ return new model(values);
606
632
  }
607
- return itemKey in resolved ? resolved[itemKey] : defaultValue ?? null;
608
- }
609
- /**
610
- * Return a config value as a string, or `defaultValue` if absent or not a string.
611
- *
612
- * @throws {SmplNotConnectedError} If connect() has not been called.
613
- */
614
- getString(configKey, itemKey, defaultValue = null) {
615
- const value = this.getValue(configKey, itemKey);
616
- return typeof value === "string" ? value : defaultValue;
633
+ return values;
617
634
  }
618
635
  /**
619
- * Return a config value as a number, or `defaultValue` if absent or not a number.
636
+ * Subscribe to a config's values returns a live proxy that
637
+ * auto-updates when the underlying config changes.
620
638
  *
621
- * @throws {SmplNotConnectedError} If connect() has not been called.
639
+ * Optionally pass a model class to map the resolved values.
622
640
  */
623
- getInt(configKey, itemKey, defaultValue = null) {
624
- const value = this.getValue(configKey, itemKey);
625
- return typeof value === "number" ? value : defaultValue;
641
+ async subscribe(key, model) {
642
+ await this._ensureInitialized();
643
+ if (!(key in this._configCache)) {
644
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
645
+ }
646
+ return new LiveConfigProxy(this, key, model);
626
647
  }
648
+ // ------------------------------------------------------------------
649
+ // Runtime: change listeners (3-level overloads)
650
+ // ------------------------------------------------------------------
627
651
  /**
628
- * Return a config value as a boolean, or `defaultValue` if absent or not a boolean.
652
+ * Register a change listener.
629
653
  *
630
- * @throws {SmplNotConnectedError} If connect() has not been called.
654
+ * - `onChange(callback)` fires for any config change (global).
655
+ * - `onChange(configKey, callback)` — fires for changes to a specific config.
656
+ * - `onChange(configKey, itemKey, callback)` — fires for a specific item.
631
657
  */
632
- getBool(configKey, itemKey, defaultValue = null) {
633
- const value = this.getValue(configKey, itemKey);
634
- return typeof value === "boolean" ? value : defaultValue;
658
+ onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
659
+ if (typeof callbackOrConfigKey === "function") {
660
+ this._listeners.push({
661
+ callback: callbackOrConfigKey,
662
+ configKey: null,
663
+ itemKey: null
664
+ });
665
+ } else if (typeof callbackOrItemKey === "function") {
666
+ this._listeners.push({
667
+ callback: callbackOrItemKey,
668
+ configKey: callbackOrConfigKey,
669
+ itemKey: null
670
+ });
671
+ } else if (typeof callbackOrItemKey === "string" && callback) {
672
+ this._listeners.push({
673
+ callback,
674
+ configKey: callbackOrConfigKey,
675
+ itemKey: callbackOrItemKey
676
+ });
677
+ }
635
678
  }
679
+ // ------------------------------------------------------------------
680
+ // Runtime: refresh
681
+ // ------------------------------------------------------------------
636
682
  /**
637
683
  * Re-fetch all configs, re-resolve values, and update the cache.
638
- *
639
- * Fires change listeners for any values that differ from the previous cache.
640
- *
641
- * @throws {SmplNotConnectedError} If connect() has not been called.
684
+ * Fires change listeners for any values that differ.
642
685
  */
643
686
  async refresh() {
644
- if (!this._connected) {
645
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
687
+ if (!this._initialized) {
688
+ throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
646
689
  }
647
690
  const environment = this._parent?._environment;
648
691
  if (!environment) {
649
692
  throw new SmplError("No environment set.");
650
693
  }
651
694
  const configs = await this.list();
695
+ this._configStore = configs;
652
696
  const newCache = {};
653
697
  for (const cfg of configs) {
654
- const chain = await cfg._buildChain(this._http);
698
+ const chain = await cfg._buildChain();
655
699
  newCache[cfg.key] = resolveChain(chain, environment);
656
700
  }
657
701
  const oldCache = this._configCache;
658
702
  this._configCache = newCache;
659
703
  this._diffAndFire(oldCache, newCache, "manual");
660
704
  }
661
- /**
662
- * Register a listener that fires when a config value changes (on refresh).
663
- *
664
- * @param callback - Called with a {@link ConfigChangeEvent} on each change.
665
- * @param options.configKey - If provided, only fire for changes to this config.
666
- * @param options.itemKey - If provided, only fire for changes to this item key.
667
- */
668
- onChange(callback, options) {
669
- this._listeners.push({
670
- callback,
671
- configKey: options?.configKey ?? null,
672
- itemKey: options?.itemKey ?? null
673
- });
705
+ // ------------------------------------------------------------------
706
+ // Runtime: lazy initialization
707
+ // ------------------------------------------------------------------
708
+ /** @internal */
709
+ async _ensureInitialized() {
710
+ if (this._initialized) return;
711
+ const environment = this._parent?._environment;
712
+ if (!environment) {
713
+ throw new SmplError("No environment set. Ensure SmplClient is configured.");
714
+ }
715
+ const configs = await this.list();
716
+ this._configStore = configs;
717
+ const cache = {};
718
+ for (const cfg of configs) {
719
+ const chain = await cfg._buildChain();
720
+ cache[cfg.key] = resolveChain(chain, environment);
721
+ }
722
+ this._configCache = cache;
723
+ this._initialized = true;
724
+ if (this._getSharedWs) {
725
+ const ws = this._getSharedWs();
726
+ ws.on("config_changed", this._handleConfigChanged);
727
+ }
728
+ }
729
+ /** @internal — called by SmplClient for backward compat. */
730
+ async _connectInternal(environment) {
731
+ if (this._initialized) return;
732
+ const configs = await this.list();
733
+ this._configStore = configs;
734
+ const cache = {};
735
+ for (const cfg of configs) {
736
+ const chain = await cfg._buildChain();
737
+ cache[cfg.key] = resolveChain(chain, environment);
738
+ }
739
+ this._configCache = cache;
740
+ this._initialized = true;
674
741
  }
742
+ /** @internal — get resolved config from cache. Used by LiveConfigProxy. */
743
+ _getCachedConfig(key) {
744
+ return this._configCache[key];
745
+ }
746
+ // ------------------------------------------------------------------
747
+ // Internal: WebSocket handler
748
+ // ------------------------------------------------------------------
749
+ _handleConfigChanged = (_data) => {
750
+ void this.refresh().catch(() => {
751
+ });
752
+ };
753
+ // ------------------------------------------------------------------
754
+ // Internal: change detection
755
+ // ------------------------------------------------------------------
675
756
  /** @internal */
676
757
  _diffAndFire(oldCache, newCache, source) {
677
758
  const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
@@ -702,54 +783,9 @@ var ConfigClient = class {
702
783
  }
703
784
  }
704
785
  }
705
- /**
706
- * Internal: PUT a full config update and return the updated model.
707
- *
708
- * Called by {@link Config} instance methods.
709
- * @internal
710
- */
711
- async _updateConfig(payload) {
712
- const body = buildRequestBody({
713
- id: payload.configId,
714
- name: payload.name,
715
- key: payload.key,
716
- description: payload.description,
717
- parent: payload.parent,
718
- items: payload.items,
719
- environments: payload.environments
720
- });
721
- let data;
722
- try {
723
- const result = await this._http.PUT("/api/v1/configs/{id}", {
724
- params: { path: { id: payload.configId } },
725
- body
726
- });
727
- if (result.error !== void 0)
728
- await checkError(result.response, `Failed to update config ${payload.configId}`);
729
- data = result.data;
730
- } catch (err) {
731
- wrapFetchError(err);
732
- }
733
- if (!data || !data.data)
734
- throw new SmplValidationError(`Failed to update config ${payload.configId}`);
735
- return resourceToConfig(data.data, this);
736
- }
737
- // ---- Private helpers ----
738
- async _getById(configId) {
739
- let data;
740
- try {
741
- const result = await this._http.GET("/api/v1/configs/{id}", {
742
- params: { path: { id: configId } }
743
- });
744
- if (result.error !== void 0)
745
- await checkError(result.response, `Config ${configId} not found`);
746
- data = result.data;
747
- } catch (err) {
748
- wrapFetchError(err);
749
- }
750
- if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
751
- return resourceToConfig(data.data, this);
752
- }
786
+ // ------------------------------------------------------------------
787
+ // Internal: fetch by key
788
+ // ------------------------------------------------------------------
753
789
  async _getByKey(key) {
754
790
  let data;
755
791
  try {
@@ -774,7 +810,7 @@ var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
774
810
 
775
811
  // src/flags/models.ts
776
812
  var Flag = class {
777
- /** UUID of the flag. */
813
+ /** UUID of the flag, or `null` if unsaved. */
778
814
  id;
779
815
  /** Unique key within the account. */
780
816
  key;
@@ -811,37 +847,35 @@ var Flag = class {
811
847
  this.updatedAt = fields.updatedAt;
812
848
  }
813
849
  /**
814
- * Update this flag's attributes on the server.
850
+ * Persist this flag to the server.
815
851
  *
816
- * Only provided fields are changed; others retain their current values.
852
+ * POST if `id` is null (new flag), PUT if `id` is set (update).
853
+ * Updates this instance in-place with the server response.
817
854
  */
818
- async update(options) {
819
- const updated = await this._client._updateFlag({
820
- flag: this,
821
- environments: options.environments,
822
- values: options.values,
823
- default: options.default,
824
- description: options.description,
825
- name: options.name
826
- });
827
- this._apply(updated);
855
+ async save() {
856
+ if (this.id === null) {
857
+ const created = await this._client._createFlag(this);
858
+ this._apply(created);
859
+ } else {
860
+ const updated = await this._client._updateFlag(this);
861
+ this._apply(updated);
862
+ }
828
863
  }
829
864
  /**
830
- * Add a rule to a specific environment.
865
+ * Add a rule to a specific environment (sync local mutation).
831
866
  *
832
867
  * The built rule must include an `environment` key (set via
833
- * `Rule(...).environment("env_key")`). Re-fetches current state
834
- * first to avoid stale data.
868
+ * `Rule(...).environment("env_key")`). No HTTP call is made.
869
+ *
870
+ * @returns `this` for chaining.
835
871
  */
836
- async addRule(builtRule) {
872
+ addRule(builtRule) {
837
873
  const envKey = builtRule.environment;
838
874
  if (!envKey) {
839
875
  throw new Error(
840
876
  `Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
841
877
  );
842
878
  }
843
- const current = await this._client.get(this.id);
844
- this._apply(current);
845
879
  const envs = { ...this.environments };
846
880
  const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
847
881
  const rules = [...envData.rules ?? []];
@@ -849,13 +883,44 @@ var Flag = class {
849
883
  rules.push(ruleCopy);
850
884
  envData.rules = rules;
851
885
  envs[envKey] = envData;
852
- const updated = await this._client._updateFlag({
853
- flag: this,
854
- environments: envs
855
- });
856
- this._apply(updated);
886
+ this.environments = envs;
887
+ return this;
857
888
  }
858
- /** @internal */
889
+ /** Enable or disable a flag in a specific environment (sync local mutation). */
890
+ setEnvironmentEnabled(envKey, enabled) {
891
+ const envs = { ...this.environments };
892
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
893
+ envData.enabled = enabled;
894
+ envs[envKey] = envData;
895
+ this.environments = envs;
896
+ }
897
+ /** Set the default value for a specific environment (sync local mutation). */
898
+ setEnvironmentDefault(envKey, defaultValue) {
899
+ const envs = { ...this.environments };
900
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
901
+ envData.default = defaultValue;
902
+ envs[envKey] = envData;
903
+ this.environments = envs;
904
+ }
905
+ /** Clear all rules for a specific environment (sync local mutation). */
906
+ clearRules(envKey) {
907
+ const envs = { ...this.environments };
908
+ const envData = envs[envKey];
909
+ if (envData) {
910
+ envs[envKey] = { ...envData, rules: [] };
911
+ this.environments = envs;
912
+ }
913
+ }
914
+ /**
915
+ * Evaluate the flag locally (sync, no HTTP).
916
+ *
917
+ * Requires `initialize()` to have been called.
918
+ */
919
+ /* v8 ignore next 3 — overridden by all exported subclasses */
920
+ get(options) {
921
+ return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
922
+ }
923
+ /** @internal — copy all fields from another Flag instance. */
859
924
  _apply(other) {
860
925
  this.id = other.id;
861
926
  this.key = other.key;
@@ -872,35 +937,52 @@ var Flag = class {
872
937
  return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
873
938
  }
874
939
  };
875
- var ContextType = class {
876
- /** UUID. */
877
- id;
878
- /** Unique key within the account. */
879
- key;
880
- /** Human-readable display name. */
881
- name;
882
- /** Known attributes. */
883
- attributes;
884
- constructor(fields) {
885
- this.id = fields.id;
886
- this.key = fields.key;
887
- this.name = fields.name;
888
- this.attributes = fields.attributes;
940
+ var BooleanFlag = class extends Flag {
941
+ get(options) {
942
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
943
+ if (typeof value === "boolean") {
944
+ return value;
945
+ }
946
+ return this.default;
889
947
  }
890
- toString() {
891
- return `ContextType(key=${this.key}, name=${this.name})`;
948
+ };
949
+ var StringFlag = class extends Flag {
950
+ get(options) {
951
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
952
+ if (typeof value === "string") {
953
+ return value;
954
+ }
955
+ return this.default;
892
956
  }
893
957
  };
894
-
895
- // src/flags/client.ts
896
- var import_json_logic_js = __toESM(require("json-logic-js"), 1);
897
- var FLAGS_BASE_URL = "https://flags.smplkit.com";
898
- var APP_BASE_URL = "https://app.smplkit.com";
899
- var CACHE_MAX_SIZE = 1e4;
900
- var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
901
- var CONTEXT_BATCH_FLUSH_SIZE = 100;
902
- async function checkError2(response, _context) {
903
- const body = await response.text().catch(() => "");
958
+ var NumberFlag = class extends Flag {
959
+ get(options) {
960
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
961
+ if (typeof value === "number") {
962
+ return value;
963
+ }
964
+ return this.default;
965
+ }
966
+ };
967
+ var JsonFlag = class extends Flag {
968
+ get(options) {
969
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
970
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
971
+ return value;
972
+ }
973
+ return this.default;
974
+ }
975
+ };
976
+
977
+ // src/flags/client.ts
978
+ var import_json_logic_js = __toESM(require("json-logic-js"), 1);
979
+ var FLAGS_BASE_URL = "https://flags.smplkit.com";
980
+ var APP_BASE_URL = "https://app.smplkit.com";
981
+ var CACHE_MAX_SIZE = 1e4;
982
+ var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
983
+ var CONTEXT_BATCH_FLUSH_SIZE = 100;
984
+ async function checkError2(response, _context) {
985
+ const body = await response.text().catch(() => "");
904
986
  throwForStatus(response.status, body);
905
987
  }
906
988
  function wrapFetchError2(err) {
@@ -1017,88 +1099,6 @@ var FlagStats = class {
1017
1099
  this.cacheMisses = cacheMisses;
1018
1100
  }
1019
1101
  };
1020
- var FlagHandleBase = class {
1021
- /** @internal */
1022
- _namespace;
1023
- /** @internal */
1024
- _key;
1025
- /** @internal */
1026
- _default;
1027
- /** @internal */
1028
- _listeners = [];
1029
- constructor(namespace, key, defaultValue) {
1030
- this._namespace = namespace;
1031
- this._key = key;
1032
- this._default = defaultValue;
1033
- }
1034
- get key() {
1035
- return this._key;
1036
- }
1037
- get default() {
1038
- return this._default;
1039
- }
1040
- /* v8 ignore next 3 — overridden by all exported subclasses */
1041
- get(options) {
1042
- return this._namespace._evaluateHandle(this._key, this._default, options?.context ?? null);
1043
- }
1044
- /** Register a flag-specific change listener. Works as a decorator. */
1045
- onChange(callback) {
1046
- this._listeners.push(callback);
1047
- return callback;
1048
- }
1049
- };
1050
- var BoolFlagHandle = class extends FlagHandleBase {
1051
- get(options) {
1052
- const value = this._namespace._evaluateHandle(
1053
- this._key,
1054
- this._default,
1055
- options?.context ?? null
1056
- );
1057
- if (typeof value === "boolean") {
1058
- return value;
1059
- }
1060
- return this._default;
1061
- }
1062
- };
1063
- var StringFlagHandle = class extends FlagHandleBase {
1064
- get(options) {
1065
- const value = this._namespace._evaluateHandle(
1066
- this._key,
1067
- this._default,
1068
- options?.context ?? null
1069
- );
1070
- if (typeof value === "string") {
1071
- return value;
1072
- }
1073
- return this._default;
1074
- }
1075
- };
1076
- var NumberFlagHandle = class extends FlagHandleBase {
1077
- get(options) {
1078
- const value = this._namespace._evaluateHandle(
1079
- this._key,
1080
- this._default,
1081
- options?.context ?? null
1082
- );
1083
- if (typeof value === "number") {
1084
- return value;
1085
- }
1086
- return this._default;
1087
- }
1088
- };
1089
- var JsonFlagHandle = class extends FlagHandleBase {
1090
- get(options) {
1091
- const value = this._namespace._evaluateHandle(
1092
- this._key,
1093
- this._default,
1094
- options?.context ?? null
1095
- );
1096
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1097
- return value;
1098
- }
1099
- return this._default;
1100
- }
1101
- };
1102
1102
  var ContextRegistrationBuffer = class {
1103
1103
  _seen = /* @__PURE__ */ new Map();
1104
1104
  _pending = [];
@@ -1142,13 +1142,14 @@ var FlagsClient = class {
1142
1142
  // Runtime state
1143
1143
  _environment = null;
1144
1144
  _flagStore = {};
1145
- _connected = false;
1145
+ _initialized = false;
1146
1146
  _cache = new ResolutionCache();
1147
1147
  _contextProvider = null;
1148
1148
  _contextBuffer = new ContextRegistrationBuffer();
1149
1149
  _handles = {};
1150
1150
  _globalListeners = [];
1151
- // Shared WebSocket (set during connect)
1151
+ _keyListeners = /* @__PURE__ */ new Map();
1152
+ // Shared WebSocket (set during initialize)
1152
1153
  _wsManager = null;
1153
1154
  _ensureWs;
1154
1155
  /** @internal — set by SmplClient after construction. */
@@ -1190,55 +1191,91 @@ var FlagsClient = class {
1190
1191
  });
1191
1192
  }
1192
1193
  // ------------------------------------------------------------------
1193
- // Management methods
1194
+ // Management: factory methods (return unsaved flags)
1194
1195
  // ------------------------------------------------------------------
1195
- /** Create a flag. */
1196
- async create(key, options) {
1197
- let values = options.values;
1198
- if (values === void 0 && options.type === "BOOLEAN") {
1199
- values = [
1196
+ /** Create an unsaved boolean flag. Call `.save()` to persist. */
1197
+ newBooleanFlag(key, options) {
1198
+ return new BooleanFlag(this, {
1199
+ id: null,
1200
+ key,
1201
+ name: options.name ?? keyToDisplayName(key),
1202
+ type: "BOOLEAN",
1203
+ default: options.default,
1204
+ values: [
1200
1205
  { name: "True", value: true },
1201
1206
  { name: "False", value: false }
1202
- ];
1203
- }
1204
- const body = {
1205
- data: {
1206
- type: "flag",
1207
- attributes: {
1208
- key,
1209
- name: options.name,
1210
- description: options.description ?? "",
1211
- type: options.type,
1212
- default: options.default,
1213
- values: values ?? []
1214
- }
1215
- }
1216
- };
1217
- let data;
1218
- try {
1219
- const result = await this._http.POST("/api/v1/flags", { body });
1220
- if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1221
- data = result.data;
1222
- } catch (err) {
1223
- wrapFetchError2(err);
1224
- }
1225
- if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1226
- return this._resourceToModel(data.data);
1207
+ ],
1208
+ description: options.description ?? null,
1209
+ environments: {},
1210
+ createdAt: null,
1211
+ updatedAt: null
1212
+ });
1213
+ }
1214
+ /** Create an unsaved string flag. Call `.save()` to persist. */
1215
+ newStringFlag(key, options) {
1216
+ return new StringFlag(this, {
1217
+ id: null,
1218
+ key,
1219
+ name: options.name ?? keyToDisplayName(key),
1220
+ type: "STRING",
1221
+ default: options.default,
1222
+ values: options.values ?? [],
1223
+ description: options.description ?? null,
1224
+ environments: {},
1225
+ createdAt: null,
1226
+ updatedAt: null
1227
+ });
1228
+ }
1229
+ /** Create an unsaved number flag. Call `.save()` to persist. */
1230
+ newNumberFlag(key, options) {
1231
+ return new NumberFlag(this, {
1232
+ id: null,
1233
+ key,
1234
+ name: options.name ?? keyToDisplayName(key),
1235
+ type: "NUMERIC",
1236
+ default: options.default,
1237
+ values: options.values ?? [],
1238
+ description: options.description ?? null,
1239
+ environments: {},
1240
+ createdAt: null,
1241
+ updatedAt: null
1242
+ });
1243
+ }
1244
+ /** Create an unsaved JSON flag. Call `.save()` to persist. */
1245
+ newJsonFlag(key, options) {
1246
+ return new JsonFlag(this, {
1247
+ id: null,
1248
+ key,
1249
+ name: options.name ?? keyToDisplayName(key),
1250
+ type: "JSON",
1251
+ default: options.default,
1252
+ values: options.values ?? [],
1253
+ description: options.description ?? null,
1254
+ environments: {},
1255
+ createdAt: null,
1256
+ updatedAt: null
1257
+ });
1227
1258
  }
1228
- /** Fetch a flag by UUID. */
1229
- async get(flagId) {
1259
+ // ------------------------------------------------------------------
1260
+ // Management: CRUD
1261
+ // ------------------------------------------------------------------
1262
+ /** Fetch a flag by key. */
1263
+ async get(key) {
1230
1264
  let data;
1231
1265
  try {
1232
- const result = await this._http.GET("/api/v1/flags/{id}", {
1233
- params: { path: { id: flagId } }
1266
+ const result = await this._http.GET("/api/v1/flags", {
1267
+ params: { query: { "filter[key]": key } }
1234
1268
  });
1235
- if (result.error !== void 0) await checkError2(result.response, `Flag ${flagId} not found`);
1269
+ if (result.error !== void 0)
1270
+ await checkError2(result.response, `Flag with key '${key}' not found`);
1236
1271
  data = result.data;
1237
1272
  } catch (err) {
1238
1273
  wrapFetchError2(err);
1239
1274
  }
1240
- if (!data || !data.data) throw new SmplNotFoundError(`Flag ${flagId} not found`);
1241
- return this._resourceToModel(data.data);
1275
+ if (!data || !data.data || data.data.length === 0) {
1276
+ throw new SmplNotFoundError(`Flag with key '${key}' not found`);
1277
+ }
1278
+ return this._resourceToModel(data.data[0]);
1242
1279
  }
1243
1280
  /** List all flags. */
1244
1281
  async list() {
@@ -1253,161 +1290,148 @@ var FlagsClient = class {
1253
1290
  if (!data) return [];
1254
1291
  return data.data.map((r) => this._resourceToModel(r));
1255
1292
  }
1256
- /** Delete a flag by UUID. */
1257
- async delete(flagId) {
1293
+ /** Delete a flag by key. */
1294
+ async delete(key) {
1295
+ const flag = await this.get(key);
1258
1296
  try {
1259
1297
  const result = await this._http.DELETE("/api/v1/flags/{id}", {
1260
- params: { path: { id: flagId } }
1298
+ params: { path: { id: flag.id } }
1261
1299
  });
1262
1300
  if (result.error !== void 0 && result.response.status !== 204)
1263
- await checkError2(result.response, `Failed to delete flag ${flagId}`);
1301
+ await checkError2(result.response, `Failed to delete flag '${key}'`);
1264
1302
  } catch (err) {
1265
1303
  wrapFetchError2(err);
1266
1304
  }
1267
1305
  }
1268
- /**
1269
- * Internal: PUT a full flag update.
1270
- * Called by {@link Flag} instance methods.
1271
- * @internal
1272
- */
1273
- async _updateFlag(options) {
1274
- const { flag } = options;
1306
+ // ------------------------------------------------------------------
1307
+ // Management: internal save methods (called by Flag.save())
1308
+ // ------------------------------------------------------------------
1309
+ /** @internal — POST a new flag. */
1310
+ async _createFlag(flag) {
1275
1311
  const body = {
1276
1312
  data: {
1277
1313
  type: "flag",
1278
1314
  attributes: {
1279
1315
  key: flag.key,
1280
- name: options.name !== void 0 ? options.name : flag.name,
1316
+ name: flag.name,
1317
+ description: flag.description ?? "",
1281
1318
  type: flag.type,
1282
- default: options.default !== void 0 ? options.default : flag.default,
1283
- values: options.values !== void 0 ? options.values : flag.values,
1284
- description: options.description !== void 0 ? options.description : flag.description ?? "",
1285
- ...options.environments !== void 0 ? { environments: options.environments } : flag.environments && Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1319
+ default: flag.default,
1320
+ values: flag.values,
1321
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1286
1322
  }
1287
1323
  }
1288
1324
  };
1289
1325
  let data;
1290
1326
  try {
1291
- const result = await this._http.PUT("/api/v1/flags/{id}", {
1292
- params: { path: { id: flag.id } },
1293
- body
1294
- });
1295
- if (result.error !== void 0)
1296
- await checkError2(result.response, `Failed to update flag ${flag.id}`);
1327
+ const result = await this._http.POST("/api/v1/flags", { body });
1328
+ if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1297
1329
  data = result.data;
1298
1330
  } catch (err) {
1299
1331
  wrapFetchError2(err);
1300
1332
  }
1301
- if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1333
+ if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1302
1334
  return this._resourceToModel(data.data);
1303
1335
  }
1304
- // ------------------------------------------------------------------
1305
- // Context type management (via generated app client)
1306
- // ------------------------------------------------------------------
1307
- /** Create a context type. */
1308
- async createContextType(key, options) {
1309
- let data;
1310
- try {
1311
- const result = await this._appHttp.POST("/api/v1/context_types", {
1312
- body: {
1313
- data: { type: "context_type", attributes: { key, name: options.name } }
1336
+ /** @internal — PUT a flag update. */
1337
+ async _updateFlag(flag) {
1338
+ const body = {
1339
+ data: {
1340
+ type: "flag",
1341
+ attributes: {
1342
+ key: flag.key,
1343
+ name: flag.name,
1344
+ type: flag.type,
1345
+ default: flag.default,
1346
+ values: flag.values,
1347
+ description: flag.description ?? "",
1348
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1314
1349
  }
1315
- });
1316
- if (result.error !== void 0)
1317
- await checkError2(result.response, "Failed to create context type");
1318
- data = result.data;
1319
- } catch (err) {
1320
- wrapFetchError2(err);
1321
- }
1322
- if (!data || !data.data) throw new SmplValidationError("Failed to create context type");
1323
- return this._parseContextType(data.data);
1324
- }
1325
- /** Update a context type (merge attributes). */
1326
- async updateContextType(ctId, options) {
1350
+ }
1351
+ };
1327
1352
  let data;
1328
1353
  try {
1329
- const result = await this._appHttp.PUT("/api/v1/context_types/{id}", {
1330
- params: { path: { id: ctId } },
1331
- body: {
1332
- data: {
1333
- type: "context_type",
1334
- attributes: { key: options.key, name: options.name, attributes: options.attributes }
1335
- }
1336
- }
1354
+ const result = await this._http.PUT("/api/v1/flags/{id}", {
1355
+ params: { path: { id: flag.id } },
1356
+ body
1337
1357
  });
1338
1358
  if (result.error !== void 0)
1339
- await checkError2(result.response, `Failed to update context type ${ctId}`);
1340
- data = result.data;
1341
- } catch (err) {
1342
- wrapFetchError2(err);
1343
- }
1344
- if (!data || !data.data) throw new SmplValidationError(`Failed to update context type ${ctId}`);
1345
- return this._parseContextType(data.data);
1346
- }
1347
- /** List all context types. */
1348
- async listContextTypes() {
1349
- let data;
1350
- try {
1351
- const result = await this._appHttp.GET("/api/v1/context_types");
1352
- if (result.error !== void 0)
1353
- await checkError2(result.response, "Failed to list context types");
1354
- data = result.data;
1355
- } catch (err) {
1356
- wrapFetchError2(err);
1357
- }
1358
- if (!data || !data.data) throw new SmplValidationError("Failed to list context types");
1359
- return data.data.map((item) => this._parseContextType(item));
1360
- }
1361
- /** Delete a context type. */
1362
- async deleteContextType(ctId) {
1363
- try {
1364
- const result = await this._appHttp.DELETE("/api/v1/context_types/{id}", {
1365
- params: { path: { id: ctId } }
1366
- });
1367
- if (result.error !== void 0 && result.response.status !== 204)
1368
- await checkError2(result.response, `Failed to delete context type ${ctId}`);
1369
- } catch (err) {
1370
- wrapFetchError2(err);
1371
- }
1372
- }
1373
- /** List context instances filtered by context type key. */
1374
- async listContexts(options) {
1375
- let data;
1376
- try {
1377
- const result = await this._appHttp.GET("/api/v1/contexts", {
1378
- params: { query: { "filter[context_type_id]": options.contextTypeKey } }
1379
- });
1380
- if (result.error !== void 0) await checkError2(result.response, "Failed to list contexts");
1359
+ await checkError2(result.response, `Failed to update flag ${flag.id}`);
1381
1360
  data = result.data;
1382
1361
  } catch (err) {
1383
1362
  wrapFetchError2(err);
1384
1363
  }
1385
- return data?.data ?? [];
1364
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1365
+ return this._resourceToModel(data.data);
1386
1366
  }
1387
1367
  // ------------------------------------------------------------------
1388
1368
  // Runtime: typed flag handles
1389
1369
  // ------------------------------------------------------------------
1390
- /** Declare a boolean flag handle. */
1391
- boolFlag(key, defaultValue) {
1392
- const handle = new BoolFlagHandle(this, key, defaultValue);
1370
+ /** Declare a boolean flag handle for runtime evaluation. */
1371
+ booleanFlag(key, defaultValue) {
1372
+ const handle = new BooleanFlag(this, {
1373
+ id: null,
1374
+ key,
1375
+ name: key,
1376
+ type: "BOOLEAN",
1377
+ default: defaultValue,
1378
+ values: [],
1379
+ description: null,
1380
+ environments: {},
1381
+ createdAt: null,
1382
+ updatedAt: null
1383
+ });
1393
1384
  this._handles[key] = handle;
1394
1385
  return handle;
1395
1386
  }
1396
- /** Declare a string flag handle. */
1387
+ /** Declare a string flag handle for runtime evaluation. */
1397
1388
  stringFlag(key, defaultValue) {
1398
- const handle = new StringFlagHandle(this, key, defaultValue);
1389
+ const handle = new StringFlag(this, {
1390
+ id: null,
1391
+ key,
1392
+ name: key,
1393
+ type: "STRING",
1394
+ default: defaultValue,
1395
+ values: [],
1396
+ description: null,
1397
+ environments: {},
1398
+ createdAt: null,
1399
+ updatedAt: null
1400
+ });
1399
1401
  this._handles[key] = handle;
1400
1402
  return handle;
1401
1403
  }
1402
- /** Declare a numeric flag handle. */
1404
+ /** Declare a numeric flag handle for runtime evaluation. */
1403
1405
  numberFlag(key, defaultValue) {
1404
- const handle = new NumberFlagHandle(this, key, defaultValue);
1406
+ const handle = new NumberFlag(this, {
1407
+ id: null,
1408
+ key,
1409
+ name: key,
1410
+ type: "NUMERIC",
1411
+ default: defaultValue,
1412
+ values: [],
1413
+ description: null,
1414
+ environments: {},
1415
+ createdAt: null,
1416
+ updatedAt: null
1417
+ });
1405
1418
  this._handles[key] = handle;
1406
1419
  return handle;
1407
1420
  }
1408
- /** Declare a JSON flag handle. */
1421
+ /** Declare a JSON flag handle for runtime evaluation. */
1409
1422
  jsonFlag(key, defaultValue) {
1410
- const handle = new JsonFlagHandle(this, key, defaultValue);
1423
+ const handle = new JsonFlag(this, {
1424
+ id: null,
1425
+ key,
1426
+ name: key,
1427
+ type: "JSON",
1428
+ default: defaultValue,
1429
+ values: [],
1430
+ description: null,
1431
+ environments: {},
1432
+ createdAt: null,
1433
+ updatedAt: null
1434
+ });
1411
1435
  this._handles[key] = handle;
1412
1436
  return handle;
1413
1437
  }
@@ -1417,41 +1441,32 @@ var FlagsClient = class {
1417
1441
  /**
1418
1442
  * Register a context provider function.
1419
1443
  *
1420
- * Called on every `handle.get()` to supply the current evaluation
1421
- * context. Can also be used as a decorator:
1422
- *
1423
- * ```typescript
1424
- * client.flags.setContextProvider(() => [
1425
- * new Context("user", userId, { plan: userPlan }),
1426
- * ]);
1427
- * ```
1444
+ * Called on every `handle.get()` to supply the current evaluation context.
1428
1445
  */
1429
1446
  setContextProvider(fn) {
1430
1447
  this._contextProvider = fn;
1431
1448
  }
1432
1449
  /**
1433
1450
  * Register a context provider — decorator-style alias.
1434
- *
1435
- * ```typescript
1436
- * const provider = client.flags.contextProvider(() => [...]);
1437
- * ```
1438
1451
  */
1439
1452
  contextProvider(fn) {
1440
1453
  this._contextProvider = fn;
1441
1454
  return fn;
1442
1455
  }
1443
1456
  // ------------------------------------------------------------------
1444
- // Runtime: connect / disconnect / refresh
1457
+ // Runtime: initialize / disconnect / refresh
1445
1458
  // ------------------------------------------------------------------
1446
1459
  /**
1447
- * Connect to an environment: fetch flag definitions, register on
1448
- * shared WebSocket, enable local evaluation.
1449
- * @internalcalled by SmplClient.connect().
1460
+ * Initialize the flags runtime: fetch definitions and wire WebSocket.
1461
+ *
1462
+ * Idempotentsafe to call multiple times. Must be called (and awaited)
1463
+ * before using `.get()` on flag handles.
1450
1464
  */
1451
- async _connectInternal(environment) {
1452
- this._environment = environment;
1465
+ async initialize() {
1466
+ if (this._initialized) return;
1467
+ this._environment = this._parent?._environment ?? null;
1453
1468
  await this._fetchAllFlags();
1454
- this._connected = true;
1469
+ this._initialized = true;
1455
1470
  this._cache.clear();
1456
1471
  this._wsManager = this._ensureWs();
1457
1472
  this._wsManager.on("flag_changed", this._handleFlagChanged);
@@ -1467,7 +1482,7 @@ var FlagsClient = class {
1467
1482
  await this._flushContexts();
1468
1483
  this._flagStore = {};
1469
1484
  this._cache.clear();
1470
- this._connected = false;
1485
+ this._initialized = false;
1471
1486
  this._environment = null;
1472
1487
  }
1473
1488
  /** Re-fetch all flag definitions and clear cache. */
@@ -1488,22 +1503,27 @@ var FlagsClient = class {
1488
1503
  return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
1489
1504
  }
1490
1505
  // ------------------------------------------------------------------
1491
- // Runtime: change listeners
1506
+ // Runtime: change listeners (dual-mode)
1492
1507
  // ------------------------------------------------------------------
1493
- /** Register a global change listener that fires for any flag change. */
1494
- onChangeAny(callback) {
1495
- this._globalListeners.push(callback);
1496
- return callback;
1497
- }
1498
1508
  /**
1499
- * Register a global change listener — decorator-style alias.
1509
+ * Register a change listener.
1500
1510
  *
1501
- * ```typescript
1502
- * const listener = client.flags.onChange((event) => { ... });
1503
- * ```
1511
+ * - `onChange(callback)` — fires for any flag change (global).
1512
+ * - `onChange(key, callback)` fires only for the specified flag key.
1504
1513
  */
1505
- onChange(callback) {
1506
- return this.onChangeAny(callback);
1514
+ onChange(callbackOrKey, callback) {
1515
+ if (typeof callbackOrKey === "function") {
1516
+ this._globalListeners.push(callbackOrKey);
1517
+ } else {
1518
+ const key = callbackOrKey;
1519
+ if (!callback) {
1520
+ throw new SmplError("onChange(key, callback) requires a callback function.");
1521
+ }
1522
+ if (!this._keyListeners.has(key)) {
1523
+ this._keyListeners.set(key, []);
1524
+ }
1525
+ this._keyListeners.get(key).push(callback);
1526
+ }
1507
1527
  }
1508
1528
  // ------------------------------------------------------------------
1509
1529
  // Runtime: context registration
@@ -1512,7 +1532,7 @@ var FlagsClient = class {
1512
1532
  * Explicitly register context(s) for background batch registration.
1513
1533
  *
1514
1534
  * Accepts a single Context or an array. Fire-and-forget — never
1515
- * blocks. Works before `connect()` is called.
1535
+ * blocks. Works before `initialize()` is called.
1516
1536
  */
1517
1537
  register(context) {
1518
1538
  if (Array.isArray(context)) {
@@ -1530,8 +1550,6 @@ var FlagsClient = class {
1530
1550
  // ------------------------------------------------------------------
1531
1551
  /**
1532
1552
  * Tier 1 explicit evaluation — stateless, no provider or cache.
1533
- *
1534
- * Useful for scripts, one-off jobs, and infrastructure code.
1535
1553
  */
1536
1554
  async evaluate(key, options) {
1537
1555
  const evalDict = contextsToEvalDict(options.context);
@@ -1539,7 +1557,7 @@ var FlagsClient = class {
1539
1557
  evalDict["service"] = { key: this._parent._service };
1540
1558
  }
1541
1559
  let flagDef = null;
1542
- if (this._connected && key in this._flagStore) {
1560
+ if (this._initialized && key in this._flagStore) {
1543
1561
  flagDef = this._flagStore[key];
1544
1562
  } else {
1545
1563
  const flags = await this._fetchFlagsList();
@@ -1560,8 +1578,8 @@ var FlagsClient = class {
1560
1578
  // ------------------------------------------------------------------
1561
1579
  /** @internal */
1562
1580
  _evaluateHandle(key, defaultValue, context) {
1563
- if (!this._connected) {
1564
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
1581
+ if (!this._initialized) {
1582
+ throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
1565
1583
  }
1566
1584
  let evalDict;
1567
1585
  if (context !== null) {
@@ -1598,6 +1616,19 @@ var FlagsClient = class {
1598
1616
  return value;
1599
1617
  }
1600
1618
  // ------------------------------------------------------------------
1619
+ // Internal: _connectInternal (called by SmplClient for backward compat)
1620
+ // ------------------------------------------------------------------
1621
+ /** @internal — called by SmplClient constructor / lazy init. */
1622
+ async _connectInternal(environment) {
1623
+ this._environment = environment;
1624
+ await this._fetchAllFlags();
1625
+ this._initialized = true;
1626
+ this._cache.clear();
1627
+ this._wsManager = this._ensureWs();
1628
+ this._wsManager.on("flag_changed", this._handleFlagChanged);
1629
+ this._wsManager.on("flag_deleted", this._handleFlagDeleted);
1630
+ }
1631
+ // ------------------------------------------------------------------
1601
1632
  // Internal: event handlers (called by SharedWebSocket)
1602
1633
  // ------------------------------------------------------------------
1603
1634
  _handleFlagChanged = (data) => {
@@ -1649,9 +1680,9 @@ var FlagsClient = class {
1649
1680
  } catch {
1650
1681
  }
1651
1682
  }
1652
- const handle = this._handles[flagKey];
1653
- if (handle) {
1654
- for (const cb of handle._listeners) {
1683
+ const keyCallbacks = this._keyListeners.get(flagKey);
1684
+ if (keyCallbacks) {
1685
+ for (const cb of keyCallbacks) {
1655
1686
  try {
1656
1687
  cb(event);
1657
1688
  } catch {
@@ -1687,10 +1718,11 @@ var FlagsClient = class {
1687
1718
  // ------------------------------------------------------------------
1688
1719
  // Internal: model conversion
1689
1720
  // ------------------------------------------------------------------
1721
+ /** @internal */
1690
1722
  _resourceToModel(resource) {
1691
1723
  const attrs = resource.attributes;
1692
1724
  return new Flag(this, {
1693
- id: resource.id ?? "",
1725
+ id: resource.id ?? null,
1694
1726
  key: attrs.key,
1695
1727
  name: attrs.name,
1696
1728
  type: attrs.type,
@@ -1714,13 +1746,567 @@ var FlagsClient = class {
1714
1746
  environments: attrs.environments ?? {}
1715
1747
  };
1716
1748
  }
1717
- _parseContextType(data) {
1718
- const attrs = data.attributes ?? {};
1719
- return new ContextType({
1720
- id: data.id ?? "",
1749
+ };
1750
+
1751
+ // src/logging/client.ts
1752
+ var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
1753
+
1754
+ // src/logging/models.ts
1755
+ var Logger = class {
1756
+ /** UUID of the logger, or `null` if unsaved. */
1757
+ id;
1758
+ /** Unique key (dot-separated hierarchy). */
1759
+ key;
1760
+ /** Human-readable display name. */
1761
+ name;
1762
+ /** Base log level, or null if inherited. */
1763
+ level;
1764
+ /** UUID of the parent log group, or null. */
1765
+ group;
1766
+ /** Whether this logger is managed by the platform. */
1767
+ managed;
1768
+ /** Observed sources (services that report this logger). */
1769
+ sources;
1770
+ /** Per-environment level overrides. */
1771
+ environments;
1772
+ /** When the logger was created. */
1773
+ createdAt;
1774
+ /** When the logger was last updated. */
1775
+ updatedAt;
1776
+ /** @internal */
1777
+ _client;
1778
+ /** @internal */
1779
+ constructor(client, fields) {
1780
+ this._client = client;
1781
+ this.id = fields.id;
1782
+ this.key = fields.key;
1783
+ this.name = fields.name;
1784
+ this.level = fields.level;
1785
+ this.group = fields.group;
1786
+ this.managed = fields.managed;
1787
+ this.sources = fields.sources;
1788
+ this.environments = fields.environments;
1789
+ this.createdAt = fields.createdAt;
1790
+ this.updatedAt = fields.updatedAt;
1791
+ }
1792
+ /**
1793
+ * Persist this logger to the server.
1794
+ *
1795
+ * POST if `id` is null (new), PUT if `id` is set (update).
1796
+ */
1797
+ async save() {
1798
+ const saved = await this._client._saveLogger(this);
1799
+ this._apply(saved);
1800
+ }
1801
+ /** Set the base log level (sync local mutation). */
1802
+ setLevel(level) {
1803
+ this.level = level;
1804
+ }
1805
+ /** Clear the base log level (sync local mutation). */
1806
+ clearLevel() {
1807
+ this.level = null;
1808
+ }
1809
+ /** Set an environment-specific log level (sync local mutation). */
1810
+ setEnvironmentLevel(env, level) {
1811
+ const envs = { ...this.environments };
1812
+ envs[env] = { ...envs[env] ?? {}, level };
1813
+ this.environments = envs;
1814
+ }
1815
+ /** Clear an environment-specific log level (sync local mutation). */
1816
+ clearEnvironmentLevel(env) {
1817
+ const envs = { ...this.environments };
1818
+ if (envs[env]) {
1819
+ const entry = { ...envs[env] };
1820
+ delete entry.level;
1821
+ envs[env] = entry;
1822
+ this.environments = envs;
1823
+ }
1824
+ }
1825
+ /** Clear all environment-specific log levels (sync local mutation). */
1826
+ clearAllEnvironmentLevels() {
1827
+ this.environments = {};
1828
+ }
1829
+ /** @internal — copy all fields from another Logger instance. */
1830
+ _apply(other) {
1831
+ this.id = other.id;
1832
+ this.key = other.key;
1833
+ this.name = other.name;
1834
+ this.level = other.level;
1835
+ this.group = other.group;
1836
+ this.managed = other.managed;
1837
+ this.sources = other.sources;
1838
+ this.environments = other.environments;
1839
+ this.createdAt = other.createdAt;
1840
+ this.updatedAt = other.updatedAt;
1841
+ }
1842
+ toString() {
1843
+ return `Logger(key=${this.key}, level=${this.level})`;
1844
+ }
1845
+ };
1846
+ var LogGroup = class {
1847
+ /** UUID of the log group, or `null` if unsaved. */
1848
+ id;
1849
+ /** Unique key. */
1850
+ key;
1851
+ /** Human-readable display name. */
1852
+ name;
1853
+ /** Base log level, or null if inherited. */
1854
+ level;
1855
+ /** UUID of the parent log group, or null. */
1856
+ group;
1857
+ /** Per-environment level overrides. */
1858
+ environments;
1859
+ /** When the log group was created. */
1860
+ createdAt;
1861
+ /** When the log group was last updated. */
1862
+ updatedAt;
1863
+ /** @internal */
1864
+ _client;
1865
+ /** @internal */
1866
+ constructor(client, fields) {
1867
+ this._client = client;
1868
+ this.id = fields.id;
1869
+ this.key = fields.key;
1870
+ this.name = fields.name;
1871
+ this.level = fields.level;
1872
+ this.group = fields.group;
1873
+ this.environments = fields.environments;
1874
+ this.createdAt = fields.createdAt;
1875
+ this.updatedAt = fields.updatedAt;
1876
+ }
1877
+ /**
1878
+ * Persist this log group to the server.
1879
+ *
1880
+ * POST if `id` is null (new), PUT if `id` is set (update).
1881
+ */
1882
+ async save() {
1883
+ const saved = await this._client._saveLogGroup(this);
1884
+ this._apply(saved);
1885
+ }
1886
+ /** Set the base log level (sync local mutation). */
1887
+ setLevel(level) {
1888
+ this.level = level;
1889
+ }
1890
+ /** Clear the base log level (sync local mutation). */
1891
+ clearLevel() {
1892
+ this.level = null;
1893
+ }
1894
+ /** Set an environment-specific log level (sync local mutation). */
1895
+ setEnvironmentLevel(env, level) {
1896
+ const envs = { ...this.environments };
1897
+ envs[env] = { ...envs[env] ?? {}, level };
1898
+ this.environments = envs;
1899
+ }
1900
+ /** Clear an environment-specific log level (sync local mutation). */
1901
+ clearEnvironmentLevel(env) {
1902
+ const envs = { ...this.environments };
1903
+ if (envs[env]) {
1904
+ const entry = { ...envs[env] };
1905
+ delete entry.level;
1906
+ envs[env] = entry;
1907
+ this.environments = envs;
1908
+ }
1909
+ }
1910
+ /** Clear all environment-specific log levels (sync local mutation). */
1911
+ clearAllEnvironmentLevels() {
1912
+ this.environments = {};
1913
+ }
1914
+ /** @internal — copy all fields from another LogGroup instance. */
1915
+ _apply(other) {
1916
+ this.id = other.id;
1917
+ this.key = other.key;
1918
+ this.name = other.name;
1919
+ this.level = other.level;
1920
+ this.group = other.group;
1921
+ this.environments = other.environments;
1922
+ this.createdAt = other.createdAt;
1923
+ this.updatedAt = other.updatedAt;
1924
+ }
1925
+ toString() {
1926
+ return `LogGroup(key=${this.key}, level=${this.level})`;
1927
+ }
1928
+ };
1929
+
1930
+ // src/logging/client.ts
1931
+ var LOGGING_BASE_URL = "https://logging.smplkit.com";
1932
+ async function checkError3(response, _context) {
1933
+ const body = await response.text().catch(() => "");
1934
+ throwForStatus(response.status, body);
1935
+ }
1936
+ function wrapFetchError3(err) {
1937
+ if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
1938
+ throw err;
1939
+ }
1940
+ if (err instanceof TypeError) {
1941
+ throw new SmplConnectionError(`Network error: ${err.message}`);
1942
+ }
1943
+ throw new SmplConnectionError(
1944
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1945
+ );
1946
+ }
1947
+ var LoggingClient = class {
1948
+ /** @internal */
1949
+ _apiKey;
1950
+ /** @internal */
1951
+ _baseUrl = LOGGING_BASE_URL;
1952
+ /** @internal */
1953
+ _http;
1954
+ /** @internal — set by SmplClient after construction. */
1955
+ _parent = null;
1956
+ _ensureWs;
1957
+ _wsManager = null;
1958
+ _started = false;
1959
+ _globalListeners = [];
1960
+ _keyListeners = /* @__PURE__ */ new Map();
1961
+ /** @internal */
1962
+ constructor(apiKey, ensureWs, timeout) {
1963
+ this._apiKey = apiKey;
1964
+ this._ensureWs = ensureWs;
1965
+ const ms = timeout ?? 3e4;
1966
+ this._http = (0, import_openapi_fetch3.default)({
1967
+ baseUrl: LOGGING_BASE_URL,
1968
+ headers: {
1969
+ Authorization: `Bearer ${apiKey}`,
1970
+ Accept: "application/json"
1971
+ },
1972
+ fetch: async (request) => {
1973
+ const controller = new AbortController();
1974
+ const timer = setTimeout(() => controller.abort(), ms);
1975
+ try {
1976
+ return await fetch(new Request(request, { signal: controller.signal }));
1977
+ } catch (err) {
1978
+ if (err instanceof DOMException && err.name === "AbortError") {
1979
+ throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1980
+ }
1981
+ throw err;
1982
+ } finally {
1983
+ clearTimeout(timer);
1984
+ }
1985
+ }
1986
+ });
1987
+ }
1988
+ // ------------------------------------------------------------------
1989
+ // Management: Logger factory
1990
+ // ------------------------------------------------------------------
1991
+ /** Create an unsaved logger. Call `.save()` to persist. */
1992
+ new(key, options) {
1993
+ return new Logger(this, {
1994
+ id: null,
1995
+ key,
1996
+ name: options?.name ?? keyToDisplayName(key),
1997
+ level: null,
1998
+ group: null,
1999
+ managed: options?.managed ?? false,
2000
+ sources: [],
2001
+ environments: {},
2002
+ createdAt: null,
2003
+ updatedAt: null
2004
+ });
2005
+ }
2006
+ // ------------------------------------------------------------------
2007
+ // Management: Logger CRUD
2008
+ // ------------------------------------------------------------------
2009
+ /** Fetch a logger by key. */
2010
+ async get(key) {
2011
+ let data;
2012
+ try {
2013
+ const result = await this._http.GET("/api/v1/loggers", {
2014
+ params: { query: { "filter[key]": key } }
2015
+ });
2016
+ if (result.error !== void 0)
2017
+ await checkError3(result.response, `Logger with key '${key}' not found`);
2018
+ data = result.data;
2019
+ } catch (err) {
2020
+ wrapFetchError3(err);
2021
+ }
2022
+ if (!data || !data.data || data.data.length === 0) {
2023
+ throw new SmplNotFoundError(`Logger with key '${key}' not found`);
2024
+ }
2025
+ return this._loggerToModel(data.data[0]);
2026
+ }
2027
+ /** List all loggers. */
2028
+ async list() {
2029
+ let data;
2030
+ try {
2031
+ const result = await this._http.GET("/api/v1/loggers", {});
2032
+ if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
2033
+ data = result.data;
2034
+ } catch (err) {
2035
+ wrapFetchError3(err);
2036
+ }
2037
+ if (!data) return [];
2038
+ return data.data.map((r) => this._loggerToModel(r));
2039
+ }
2040
+ /** Delete a logger by key. */
2041
+ async delete(key) {
2042
+ const logger = await this.get(key);
2043
+ try {
2044
+ const result = await this._http.DELETE("/api/v1/loggers/{id}", {
2045
+ params: { path: { id: logger.id } }
2046
+ });
2047
+ if (result.error !== void 0 && result.response.status !== 204)
2048
+ await checkError3(result.response, `Failed to delete logger '${key}'`);
2049
+ } catch (err) {
2050
+ wrapFetchError3(err);
2051
+ }
2052
+ }
2053
+ // ------------------------------------------------------------------
2054
+ // Management: LogGroup factory
2055
+ // ------------------------------------------------------------------
2056
+ /** Create an unsaved log group. Call `.save()` to persist. */
2057
+ newGroup(key, options) {
2058
+ return new LogGroup(this, {
2059
+ id: null,
2060
+ key,
2061
+ name: options?.name ?? keyToDisplayName(key),
2062
+ level: null,
2063
+ group: options?.group ?? null,
2064
+ environments: {},
2065
+ createdAt: null,
2066
+ updatedAt: null
2067
+ });
2068
+ }
2069
+ // ------------------------------------------------------------------
2070
+ // Management: LogGroup CRUD
2071
+ // ------------------------------------------------------------------
2072
+ /** Fetch a log group by key. */
2073
+ async getGroup(key) {
2074
+ const groups = await this.listGroups();
2075
+ const match = groups.find((g) => g.key === key);
2076
+ if (!match) {
2077
+ throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
2078
+ }
2079
+ return match;
2080
+ }
2081
+ /** List all log groups. */
2082
+ async listGroups() {
2083
+ let data;
2084
+ try {
2085
+ const result = await this._http.GET("/api/v1/log_groups", {});
2086
+ if (result.error !== void 0)
2087
+ await checkError3(result.response, "Failed to list log groups");
2088
+ data = result.data;
2089
+ } catch (err) {
2090
+ wrapFetchError3(err);
2091
+ }
2092
+ if (!data) return [];
2093
+ return data.data.map((r) => this._groupToModel(r));
2094
+ }
2095
+ /** Delete a log group by key. */
2096
+ async deleteGroup(key) {
2097
+ const group = await this.getGroup(key);
2098
+ try {
2099
+ const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
2100
+ params: { path: { id: group.id } }
2101
+ });
2102
+ if (result.error !== void 0 && result.response.status !== 204)
2103
+ await checkError3(result.response, `Failed to delete log group '${key}'`);
2104
+ } catch (err) {
2105
+ wrapFetchError3(err);
2106
+ }
2107
+ }
2108
+ // ------------------------------------------------------------------
2109
+ // Management: internal save methods
2110
+ // ------------------------------------------------------------------
2111
+ /** @internal — POST or PUT a logger. */
2112
+ async _saveLogger(logger) {
2113
+ const body = {
2114
+ data: {
2115
+ type: "logger",
2116
+ attributes: {
2117
+ key: logger.key,
2118
+ name: logger.name,
2119
+ level: logger.level,
2120
+ group: logger.group,
2121
+ managed: logger.managed,
2122
+ environments: logger.environments
2123
+ }
2124
+ }
2125
+ };
2126
+ if (logger.id === null) {
2127
+ let data;
2128
+ try {
2129
+ const result = await this._http.POST("/api/v1/loggers", { body });
2130
+ if (result.error !== void 0)
2131
+ await checkError3(result.response, "Failed to create logger");
2132
+ data = result.data;
2133
+ } catch (err) {
2134
+ wrapFetchError3(err);
2135
+ }
2136
+ if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
2137
+ return this._loggerToModel(data.data);
2138
+ } else {
2139
+ let data;
2140
+ try {
2141
+ const result = await this._http.PUT("/api/v1/loggers/{id}", {
2142
+ params: { path: { id: logger.id } },
2143
+ body
2144
+ });
2145
+ if (result.error !== void 0)
2146
+ await checkError3(result.response, `Failed to update logger ${logger.id}`);
2147
+ data = result.data;
2148
+ } catch (err) {
2149
+ wrapFetchError3(err);
2150
+ }
2151
+ if (!data || !data.data)
2152
+ throw new SmplValidationError(`Failed to update logger ${logger.id}`);
2153
+ return this._loggerToModel(data.data);
2154
+ }
2155
+ }
2156
+ /** @internal — POST or PUT a log group. */
2157
+ async _saveLogGroup(group) {
2158
+ const body = {
2159
+ data: {
2160
+ type: "log_group",
2161
+ attributes: {
2162
+ key: group.key,
2163
+ name: group.name,
2164
+ level: group.level,
2165
+ group: group.group,
2166
+ environments: group.environments
2167
+ }
2168
+ }
2169
+ };
2170
+ if (group.id === null) {
2171
+ let data;
2172
+ try {
2173
+ const result = await this._http.POST("/api/v1/log_groups", { body });
2174
+ if (result.error !== void 0)
2175
+ await checkError3(result.response, "Failed to create log group");
2176
+ data = result.data;
2177
+ } catch (err) {
2178
+ wrapFetchError3(err);
2179
+ }
2180
+ if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
2181
+ return this._groupToModel(data.data);
2182
+ } else {
2183
+ let data;
2184
+ try {
2185
+ const result = await this._http.PUT("/api/v1/log_groups/{id}", {
2186
+ params: { path: { id: group.id } },
2187
+ body
2188
+ });
2189
+ if (result.error !== void 0)
2190
+ await checkError3(result.response, `Failed to update log group ${group.id}`);
2191
+ data = result.data;
2192
+ } catch (err) {
2193
+ wrapFetchError3(err);
2194
+ }
2195
+ if (!data || !data.data)
2196
+ throw new SmplValidationError(`Failed to update log group ${group.id}`);
2197
+ return this._groupToModel(data.data);
2198
+ }
2199
+ }
2200
+ // ------------------------------------------------------------------
2201
+ // Runtime: start (scaffolded)
2202
+ // ------------------------------------------------------------------
2203
+ /**
2204
+ * Start the logging runtime.
2205
+ *
2206
+ * Fetches existing loggers/groups and wires WebSocket listeners for
2207
+ * live updates. Idempotent — safe to call multiple times.
2208
+ *
2209
+ * Note: Node.js auto-discovery (equivalent to Python's logging module
2210
+ * monkey-patching) is deferred. Management methods work without start().
2211
+ */
2212
+ async start() {
2213
+ if (this._started) return;
2214
+ this._wsManager = this._ensureWs();
2215
+ this._wsManager.on("logger_changed", this._handleLoggerChanged);
2216
+ this._started = true;
2217
+ }
2218
+ // ------------------------------------------------------------------
2219
+ // Runtime: change listeners (dual-mode)
2220
+ // ------------------------------------------------------------------
2221
+ /**
2222
+ * Register a change listener.
2223
+ *
2224
+ * - `onChange(callback)` — fires for any logger change (global).
2225
+ * - `onChange(key, callback)` — fires only for the specified logger key.
2226
+ */
2227
+ onChange(callbackOrKey, callback) {
2228
+ if (typeof callbackOrKey === "function") {
2229
+ this._globalListeners.push(callbackOrKey);
2230
+ } else {
2231
+ const key = callbackOrKey;
2232
+ if (!callback) {
2233
+ throw new SmplError("onChange(key, callback) requires a callback function.");
2234
+ }
2235
+ if (!this._keyListeners.has(key)) {
2236
+ this._keyListeners.set(key, []);
2237
+ }
2238
+ this._keyListeners.get(key).push(callback);
2239
+ }
2240
+ }
2241
+ // ------------------------------------------------------------------
2242
+ // Internal: close
2243
+ // ------------------------------------------------------------------
2244
+ /** @internal */
2245
+ _close() {
2246
+ if (this._wsManager !== null) {
2247
+ this._wsManager.off("logger_changed", this._handleLoggerChanged);
2248
+ this._wsManager = null;
2249
+ }
2250
+ this._started = false;
2251
+ }
2252
+ // ------------------------------------------------------------------
2253
+ // Internal: WebSocket handler
2254
+ // ------------------------------------------------------------------
2255
+ _handleLoggerChanged = (data) => {
2256
+ const key = data.key;
2257
+ if (key) {
2258
+ const level = data.level ?? null;
2259
+ const event = {
2260
+ key,
2261
+ level,
2262
+ source: "websocket"
2263
+ };
2264
+ for (const cb of this._globalListeners) {
2265
+ try {
2266
+ cb(event);
2267
+ } catch {
2268
+ }
2269
+ }
2270
+ const keyCallbacks = this._keyListeners.get(key);
2271
+ if (keyCallbacks) {
2272
+ for (const cb of keyCallbacks) {
2273
+ try {
2274
+ cb(event);
2275
+ } catch {
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ };
2281
+ // ------------------------------------------------------------------
2282
+ // Internal: model conversion
2283
+ // ------------------------------------------------------------------
2284
+ _loggerToModel(resource) {
2285
+ const attrs = resource.attributes;
2286
+ return new Logger(this, {
2287
+ id: resource.id ?? null,
1721
2288
  key: attrs.key ?? "",
1722
- name: attrs.name ?? "",
1723
- attributes: attrs.attributes ?? {}
2289
+ name: attrs.name,
2290
+ level: attrs.level ?? null,
2291
+ group: attrs.group ?? null,
2292
+ managed: attrs.managed ?? false,
2293
+ sources: attrs.sources ?? [],
2294
+ environments: attrs.environments ?? {},
2295
+ createdAt: attrs.created_at ?? null,
2296
+ updatedAt: attrs.updated_at ?? null
2297
+ });
2298
+ }
2299
+ _groupToModel(resource) {
2300
+ const attrs = resource.attributes;
2301
+ return new LogGroup(this, {
2302
+ id: resource.id ?? null,
2303
+ key: attrs.key ?? "",
2304
+ name: attrs.name,
2305
+ level: attrs.level ?? null,
2306
+ group: attrs.group ?? null,
2307
+ environments: attrs.environments ?? {},
2308
+ createdAt: attrs.created_at ?? null,
2309
+ updatedAt: attrs.updated_at ?? null
1724
2310
  });
1725
2311
  }
1726
2312
  };
@@ -1936,17 +2522,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
1936
2522
  var NO_ENVIRONMENT_MESSAGE = "No environment provided. Set one of:\n 1. Pass environment to the constructor\n 2. Set the SMPLKIT_ENVIRONMENT environment variable";
1937
2523
  var NO_SERVICE_MESSAGE = "No service provided. Set one of:\n 1. Pass service in options\n 2. Set the SMPLKIT_SERVICE environment variable";
1938
2524
  var SmplClient = class {
1939
- /** Client for config management-plane operations. */
2525
+ /** Client for config management and runtime. */
1940
2526
  config;
1941
- /** Client for flags management and runtime operations. */
2527
+ /** Client for flags management and runtime. */
1942
2528
  flags;
2529
+ /** Client for logging management and runtime. */
2530
+ logging;
1943
2531
  _wsManager = null;
1944
2532
  _apiKey;
1945
2533
  /** @internal */
1946
2534
  _environment;
1947
2535
  /** @internal */
1948
2536
  _service;
1949
- _connected = false;
1950
2537
  _timeout;
1951
2538
  _appHttp;
1952
2539
  constructor(options = {}) {
@@ -1964,7 +2551,7 @@ var SmplClient = class {
1964
2551
  this._apiKey = apiKey;
1965
2552
  this._timeout = options.timeout ?? 3e4;
1966
2553
  const ms = this._timeout;
1967
- this._appHttp = (0, import_openapi_fetch3.default)({
2554
+ this._appHttp = (0, import_openapi_fetch4.default)({
1968
2555
  baseUrl: APP_BASE_URL2,
1969
2556
  headers: {
1970
2557
  Authorization: `Bearer ${apiKey}`,
@@ -1987,24 +2574,12 @@ var SmplClient = class {
1987
2574
  });
1988
2575
  this.config = new ConfigClient(apiKey, this._timeout);
1989
2576
  this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
2577
+ this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
1990
2578
  this.config._getSharedWs = () => this._ensureWs();
1991
2579
  this.flags._parent = this;
1992
2580
  this.config._parent = this;
1993
- }
1994
- /**
1995
- * Connect to the smplkit platform.
1996
- *
1997
- * Fetches initial flag and config data, opens the shared WebSocket,
1998
- * and registers the service as a context instance (if provided).
1999
- *
2000
- * This method is idempotent — calling it multiple times is safe.
2001
- */
2002
- async connect() {
2003
- if (this._connected) return;
2004
- await this._registerServiceContext();
2005
- await this.flags._connectInternal(this._environment);
2006
- await this.config._connectInternal(this._environment);
2007
- this._connected = true;
2581
+ this.logging._parent = this;
2582
+ void this._registerServiceContext();
2008
2583
  }
2009
2584
  /** @internal */
2010
2585
  async _registerServiceContext() {
@@ -2033,6 +2608,7 @@ var SmplClient = class {
2033
2608
  }
2034
2609
  /** Close the shared WebSocket and release resources. */
2035
2610
  close() {
2611
+ this.logging._close();
2036
2612
  if (this._wsManager !== null) {
2037
2613
  this._wsManager.stop();
2038
2614
  this._wsManager = null;
@@ -2104,29 +2680,44 @@ var Rule = class {
2104
2680
  return result;
2105
2681
  }
2106
2682
  };
2683
+
2684
+ // src/logging/types.ts
2685
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2686
+ LogLevel2["TRACE"] = "TRACE";
2687
+ LogLevel2["DEBUG"] = "DEBUG";
2688
+ LogLevel2["INFO"] = "INFO";
2689
+ LogLevel2["WARN"] = "WARN";
2690
+ LogLevel2["ERROR"] = "ERROR";
2691
+ LogLevel2["FATAL"] = "FATAL";
2692
+ LogLevel2["SILENT"] = "SILENT";
2693
+ return LogLevel2;
2694
+ })(LogLevel || {});
2107
2695
  // Annotate the CommonJS export names for ESM import in node:
2108
2696
  0 && (module.exports = {
2109
- BoolFlagHandle,
2697
+ BooleanFlag,
2110
2698
  Config,
2111
2699
  ConfigClient,
2112
2700
  Context,
2113
- ContextType,
2114
2701
  Flag,
2115
2702
  FlagChangeEvent,
2116
2703
  FlagStats,
2117
2704
  FlagsClient,
2118
- JsonFlagHandle,
2119
- NumberFlagHandle,
2705
+ JsonFlag,
2706
+ LiveConfigProxy,
2707
+ LogGroup,
2708
+ LogLevel,
2709
+ Logger,
2710
+ LoggingClient,
2711
+ NumberFlag,
2120
2712
  Rule,
2121
2713
  SharedWebSocket,
2122
2714
  SmplClient,
2123
2715
  SmplConflictError,
2124
2716
  SmplConnectionError,
2125
2717
  SmplError,
2126
- SmplNotConnectedError,
2127
2718
  SmplNotFoundError,
2128
2719
  SmplTimeoutError,
2129
2720
  SmplValidationError,
2130
- StringFlagHandle
2721
+ StringFlag
2131
2722
  });
2132
2723
  //# sourceMappingURL=index.cjs.map