@smplkit/sdk 1.3.12 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,7 @@ var ConfigClient = class {
469
464
  /** @internal — set by SmplClient after construction. */
470
465
  _parent = null;
471
466
  _configCache = {};
472
- _connected = false;
467
+ _initialized = false;
473
468
  _listeners = [];
474
469
  /** @internal */
475
470
  constructor(apiKey, timeout) {
@@ -481,7 +476,6 @@ var ConfigClient = class {
481
476
  Authorization: `Bearer ${apiKey}`,
482
477
  Accept: "application/json"
483
478
  },
484
- // openapi-fetch custom fetch receives a pre-built Request object
485
479
  fetch: async (request) => {
486
480
  const controller = new AbortController();
487
481
  const timer = setTimeout(() => controller.abort(), ms);
@@ -498,23 +492,31 @@ var ConfigClient = class {
498
492
  }
499
493
  });
500
494
  }
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);
495
+ // ------------------------------------------------------------------
496
+ // Management: factory method
497
+ // ------------------------------------------------------------------
498
+ /** Create an unsaved config. Call `.save()` to persist. */
499
+ new(key, options) {
500
+ return new Config(this, {
501
+ id: null,
502
+ key,
503
+ name: options?.name ?? keyToDisplayName(key),
504
+ description: options?.description ?? null,
505
+ parent: options?.parent ?? null,
506
+ items: {},
507
+ environments: {},
508
+ createdAt: null,
509
+ updatedAt: null
510
+ });
514
511
  }
515
- /**
516
- * List all configs for the account.
517
- */
512
+ // ------------------------------------------------------------------
513
+ // Management: CRUD
514
+ // ------------------------------------------------------------------
515
+ /** Fetch a config by key. */
516
+ async get(key) {
517
+ return this._getByKey(key);
518
+ }
519
+ /** List all configs. */
518
520
  async list() {
519
521
  let data;
520
522
  try {
@@ -527,18 +529,31 @@ var ConfigClient = class {
527
529
  if (!data) return [];
528
530
  return data.data.map((r) => resourceToConfig(r, this));
529
531
  }
530
- /**
531
- * Create a new config.
532
- *
533
- * @throws {SmplValidationError} If the server rejects the request.
534
- */
535
- async create(options) {
532
+ /** Delete a config by key. */
533
+ async delete(key) {
534
+ const config = await this.get(key);
535
+ try {
536
+ const result = await this._http.DELETE("/api/v1/configs/{id}", {
537
+ params: { path: { id: config.id } }
538
+ });
539
+ if (result.error !== void 0 && result.response.status !== 204)
540
+ await checkError(result.response, `Failed to delete config '${key}'`);
541
+ } catch (err) {
542
+ wrapFetchError(err);
543
+ }
544
+ }
545
+ // ------------------------------------------------------------------
546
+ // Management: internal save methods (called by Config.save())
547
+ // ------------------------------------------------------------------
548
+ /** @internal — POST a new config. */
549
+ async _createConfig(config) {
536
550
  const body = buildRequestBody({
537
- name: options.name,
538
- key: options.key,
539
- description: options.description,
540
- parent: options.parent,
541
- items: options.items
551
+ name: config.name,
552
+ key: config.key,
553
+ description: config.description,
554
+ parent: config.parent,
555
+ items: config.items,
556
+ environments: config.environments
542
557
  });
543
558
  let data;
544
559
  try {
@@ -551,98 +566,124 @@ var ConfigClient = class {
551
566
  if (!data || !data.data) throw new SmplValidationError("Failed to create config");
552
567
  return resourceToConfig(data.data, this);
553
568
  }
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) {
569
+ /** @internal — PUT a config update. */
570
+ async _updateConfig(config) {
571
+ const body = buildRequestBody({
572
+ id: config.id,
573
+ name: config.name,
574
+ key: config.key,
575
+ description: config.description,
576
+ parent: config.parent,
577
+ items: config.items,
578
+ environments: config.environments
579
+ });
580
+ let data;
561
581
  try {
562
- const result = await this._http.DELETE("/api/v1/configs/{id}", {
563
- params: { path: { id: configId } }
582
+ const result = await this._http.PUT("/api/v1/configs/{id}", {
583
+ params: { path: { id: config.id } },
584
+ body
564
585
  });
565
- if (result.error !== void 0 && result.response.status !== 204)
566
- await checkError(result.response, `Failed to delete config ${configId}`);
586
+ if (result.error !== void 0)
587
+ await checkError(result.response, `Failed to update config ${config.id}`);
588
+ data = result.data;
567
589
  } catch (err) {
568
590
  wrapFetchError(err);
569
591
  }
592
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
593
+ return resourceToConfig(data.data, this);
570
594
  }
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);
595
+ /** @internal — fetch a config by UUID. */
596
+ async _getById(configId) {
597
+ let data;
598
+ try {
599
+ const result = await this._http.GET("/api/v1/configs/{id}", {
600
+ params: { path: { id: configId } }
601
+ });
602
+ if (result.error !== void 0)
603
+ await checkError(result.response, `Config ${configId} not found`);
604
+ data = result.data;
605
+ } catch (err) {
606
+ wrapFetchError(err);
581
607
  }
582
- this._configCache = cache;
583
- this._connected = true;
608
+ if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
609
+ return resourceToConfig(data.data, this);
584
610
  }
611
+ // ------------------------------------------------------------------
612
+ // Runtime: resolve and subscribe
613
+ // ------------------------------------------------------------------
585
614
  /**
586
- * Read a resolved config value (prescriptive access).
615
+ * Resolve a config's values for the current environment.
587
616
  *
588
- * Requires {@link SmplClient.connect} to have been called.
617
+ * Returns a flat dict of resolved key-value pairs, walking the
618
+ * parent chain and applying environment overrides.
589
619
  *
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.
620
+ * Optionally pass a model class to map the resolved values.
595
621
  */
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;
622
+ async resolve(key, model) {
623
+ await this._ensureInitialized();
624
+ const values = this._configCache[key];
625
+ if (values === void 0) {
626
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
603
627
  }
604
- if (itemKey === void 0) {
605
- return { ...resolved };
628
+ if (model) {
629
+ return new model(values);
606
630
  }
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;
631
+ return values;
617
632
  }
618
633
  /**
619
- * Return a config value as a number, or `defaultValue` if absent or not a number.
634
+ * Subscribe to a config's values returns a live proxy that
635
+ * auto-updates when the underlying config changes.
620
636
  *
621
- * @throws {SmplNotConnectedError} If connect() has not been called.
637
+ * Optionally pass a model class to map the resolved values.
622
638
  */
623
- getInt(configKey, itemKey, defaultValue = null) {
624
- const value = this.getValue(configKey, itemKey);
625
- return typeof value === "number" ? value : defaultValue;
639
+ async subscribe(key, model) {
640
+ await this._ensureInitialized();
641
+ if (!(key in this._configCache)) {
642
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
643
+ }
644
+ return new LiveConfigProxy(this, key, model);
626
645
  }
646
+ // ------------------------------------------------------------------
647
+ // Runtime: change listeners (3-level overloads)
648
+ // ------------------------------------------------------------------
627
649
  /**
628
- * Return a config value as a boolean, or `defaultValue` if absent or not a boolean.
650
+ * Register a change listener.
629
651
  *
630
- * @throws {SmplNotConnectedError} If connect() has not been called.
652
+ * - `onChange(callback)` fires for any config change (global).
653
+ * - `onChange(configKey, callback)` — fires for changes to a specific config.
654
+ * - `onChange(configKey, itemKey, callback)` — fires for a specific item.
631
655
  */
632
- getBool(configKey, itemKey, defaultValue = null) {
633
- const value = this.getValue(configKey, itemKey);
634
- return typeof value === "boolean" ? value : defaultValue;
656
+ onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
657
+ if (typeof callbackOrConfigKey === "function") {
658
+ this._listeners.push({
659
+ callback: callbackOrConfigKey,
660
+ configKey: null,
661
+ itemKey: null
662
+ });
663
+ } else if (typeof callbackOrItemKey === "function") {
664
+ this._listeners.push({
665
+ callback: callbackOrItemKey,
666
+ configKey: callbackOrConfigKey,
667
+ itemKey: null
668
+ });
669
+ } else if (typeof callbackOrItemKey === "string" && callback) {
670
+ this._listeners.push({
671
+ callback,
672
+ configKey: callbackOrConfigKey,
673
+ itemKey: callbackOrItemKey
674
+ });
675
+ }
635
676
  }
677
+ // ------------------------------------------------------------------
678
+ // Runtime: refresh
679
+ // ------------------------------------------------------------------
636
680
  /**
637
681
  * 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.
682
+ * Fires change listeners for any values that differ.
642
683
  */
643
684
  async refresh() {
644
- if (!this._connected) {
645
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
685
+ if (!this._initialized) {
686
+ throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
646
687
  }
647
688
  const environment = this._parent?._environment;
648
689
  if (!environment) {
@@ -651,27 +692,62 @@ var ConfigClient = class {
651
692
  const configs = await this.list();
652
693
  const newCache = {};
653
694
  for (const cfg of configs) {
654
- const chain = await cfg._buildChain(this._http);
695
+ const chain = await cfg._buildChain();
655
696
  newCache[cfg.key] = resolveChain(chain, environment);
656
697
  }
657
698
  const oldCache = this._configCache;
658
699
  this._configCache = newCache;
659
700
  this._diffAndFire(oldCache, newCache, "manual");
660
701
  }
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
- });
702
+ // ------------------------------------------------------------------
703
+ // Runtime: lazy initialization
704
+ // ------------------------------------------------------------------
705
+ /** @internal */
706
+ async _ensureInitialized() {
707
+ if (this._initialized) return;
708
+ const environment = this._parent?._environment;
709
+ if (!environment) {
710
+ throw new SmplError("No environment set. Ensure SmplClient is configured.");
711
+ }
712
+ const configs = await this.list();
713
+ const cache = {};
714
+ for (const cfg of configs) {
715
+ const chain = await cfg._buildChain();
716
+ cache[cfg.key] = resolveChain(chain, environment);
717
+ }
718
+ this._configCache = cache;
719
+ this._initialized = true;
720
+ if (this._getSharedWs) {
721
+ const ws = this._getSharedWs();
722
+ ws.on("config_changed", this._handleConfigChanged);
723
+ }
724
+ }
725
+ /** @internal — called by SmplClient for backward compat. */
726
+ async _connectInternal(environment) {
727
+ if (this._initialized) return;
728
+ const configs = await this.list();
729
+ const cache = {};
730
+ for (const cfg of configs) {
731
+ const chain = await cfg._buildChain();
732
+ cache[cfg.key] = resolveChain(chain, environment);
733
+ }
734
+ this._configCache = cache;
735
+ this._initialized = true;
674
736
  }
737
+ /** @internal — get resolved config from cache. Used by LiveConfigProxy. */
738
+ _getCachedConfig(key) {
739
+ return this._configCache[key];
740
+ }
741
+ // ------------------------------------------------------------------
742
+ // Internal: WebSocket handler
743
+ // ------------------------------------------------------------------
744
+ _handleConfigChanged = (_data) => {
745
+ void this.refresh().catch(() => {
746
+ });
747
+ };
748
+ // ------------------------------------------------------------------
749
+ // Internal: change detection
750
+ // ------------------------------------------------------------------
675
751
  /** @internal */
676
752
  _diffAndFire(oldCache, newCache, source) {
677
753
  const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
@@ -702,54 +778,9 @@ var ConfigClient = class {
702
778
  }
703
779
  }
704
780
  }
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
- }
781
+ // ------------------------------------------------------------------
782
+ // Internal: fetch by key
783
+ // ------------------------------------------------------------------
753
784
  async _getByKey(key) {
754
785
  let data;
755
786
  try {
@@ -774,7 +805,7 @@ var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
774
805
 
775
806
  // src/flags/models.ts
776
807
  var Flag = class {
777
- /** UUID of the flag. */
808
+ /** UUID of the flag, or `null` if unsaved. */
778
809
  id;
779
810
  /** Unique key within the account. */
780
811
  key;
@@ -811,37 +842,35 @@ var Flag = class {
811
842
  this.updatedAt = fields.updatedAt;
812
843
  }
813
844
  /**
814
- * Update this flag's attributes on the server.
845
+ * Persist this flag to the server.
815
846
  *
816
- * Only provided fields are changed; others retain their current values.
847
+ * POST if `id` is null (new flag), PUT if `id` is set (update).
848
+ * Updates this instance in-place with the server response.
817
849
  */
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);
850
+ async save() {
851
+ if (this.id === null) {
852
+ const created = await this._client._createFlag(this);
853
+ this._apply(created);
854
+ } else {
855
+ const updated = await this._client._updateFlag(this);
856
+ this._apply(updated);
857
+ }
828
858
  }
829
859
  /**
830
- * Add a rule to a specific environment.
860
+ * Add a rule to a specific environment (sync local mutation).
831
861
  *
832
862
  * 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.
863
+ * `Rule(...).environment("env_key")`). No HTTP call is made.
864
+ *
865
+ * @returns `this` for chaining.
835
866
  */
836
- async addRule(builtRule) {
867
+ addRule(builtRule) {
837
868
  const envKey = builtRule.environment;
838
869
  if (!envKey) {
839
870
  throw new Error(
840
871
  `Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
841
872
  );
842
873
  }
843
- const current = await this._client.get(this.id);
844
- this._apply(current);
845
874
  const envs = { ...this.environments };
846
875
  const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
847
876
  const rules = [...envData.rules ?? []];
@@ -849,13 +878,43 @@ var Flag = class {
849
878
  rules.push(ruleCopy);
850
879
  envData.rules = rules;
851
880
  envs[envKey] = envData;
852
- const updated = await this._client._updateFlag({
853
- flag: this,
854
- environments: envs
855
- });
856
- this._apply(updated);
881
+ this.environments = envs;
882
+ return this;
857
883
  }
858
- /** @internal */
884
+ /** Enable or disable a flag in a specific environment (sync local mutation). */
885
+ setEnvironmentEnabled(envKey, enabled) {
886
+ const envs = { ...this.environments };
887
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
888
+ envData.enabled = enabled;
889
+ envs[envKey] = envData;
890
+ this.environments = envs;
891
+ }
892
+ /** Set the default value for a specific environment (sync local mutation). */
893
+ setEnvironmentDefault(envKey, defaultValue) {
894
+ const envs = { ...this.environments };
895
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
896
+ envData.default = defaultValue;
897
+ envs[envKey] = envData;
898
+ this.environments = envs;
899
+ }
900
+ /** Clear all rules for a specific environment (sync local mutation). */
901
+ clearRules(envKey) {
902
+ const envs = { ...this.environments };
903
+ const envData = envs[envKey];
904
+ if (envData) {
905
+ envs[envKey] = { ...envData, rules: [] };
906
+ this.environments = envs;
907
+ }
908
+ }
909
+ /**
910
+ * Evaluate the flag locally (sync, no HTTP).
911
+ *
912
+ * Requires `initialize()` to have been called.
913
+ */
914
+ get(options) {
915
+ return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
916
+ }
917
+ /** @internal — copy all fields from another Flag instance. */
859
918
  _apply(other) {
860
919
  this.id = other.id;
861
920
  this.key = other.key;
@@ -872,36 +931,53 @@ var Flag = class {
872
931
  return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
873
932
  }
874
933
  };
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;
934
+ var BooleanFlag = class extends Flag {
935
+ get(options) {
936
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
937
+ if (typeof value === "boolean") {
938
+ return value;
939
+ }
940
+ return this.default;
889
941
  }
890
- toString() {
891
- return `ContextType(key=${this.key}, name=${this.name})`;
942
+ };
943
+ var StringFlag = class extends Flag {
944
+ get(options) {
945
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
946
+ if (typeof value === "string") {
947
+ return value;
948
+ }
949
+ return this.default;
892
950
  }
893
951
  };
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(() => "");
904
- throwForStatus(response.status, body);
952
+ var NumberFlag = class extends Flag {
953
+ get(options) {
954
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
955
+ if (typeof value === "number") {
956
+ return value;
957
+ }
958
+ return this.default;
959
+ }
960
+ };
961
+ var JsonFlag = class extends Flag {
962
+ get(options) {
963
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
964
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
965
+ return value;
966
+ }
967
+ return this.default;
968
+ }
969
+ };
970
+
971
+ // src/flags/client.ts
972
+ var import_json_logic_js = __toESM(require("json-logic-js"), 1);
973
+ var FLAGS_BASE_URL = "https://flags.smplkit.com";
974
+ var APP_BASE_URL = "https://app.smplkit.com";
975
+ var CACHE_MAX_SIZE = 1e4;
976
+ var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
977
+ var CONTEXT_BATCH_FLUSH_SIZE = 100;
978
+ async function checkError2(response, _context) {
979
+ const body = await response.text().catch(() => "");
980
+ throwForStatus(response.status, body);
905
981
  }
906
982
  function wrapFetchError2(err) {
907
983
  if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
@@ -1017,88 +1093,6 @@ var FlagStats = class {
1017
1093
  this.cacheMisses = cacheMisses;
1018
1094
  }
1019
1095
  };
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
1096
  var ContextRegistrationBuffer = class {
1103
1097
  _seen = /* @__PURE__ */ new Map();
1104
1098
  _pending = [];
@@ -1142,13 +1136,14 @@ var FlagsClient = class {
1142
1136
  // Runtime state
1143
1137
  _environment = null;
1144
1138
  _flagStore = {};
1145
- _connected = false;
1139
+ _initialized = false;
1146
1140
  _cache = new ResolutionCache();
1147
1141
  _contextProvider = null;
1148
1142
  _contextBuffer = new ContextRegistrationBuffer();
1149
1143
  _handles = {};
1150
1144
  _globalListeners = [];
1151
- // Shared WebSocket (set during connect)
1145
+ _keyListeners = /* @__PURE__ */ new Map();
1146
+ // Shared WebSocket (set during initialize)
1152
1147
  _wsManager = null;
1153
1148
  _ensureWs;
1154
1149
  /** @internal — set by SmplClient after construction. */
@@ -1190,55 +1185,91 @@ var FlagsClient = class {
1190
1185
  });
1191
1186
  }
1192
1187
  // ------------------------------------------------------------------
1193
- // Management methods
1188
+ // Management: factory methods (return unsaved flags)
1194
1189
  // ------------------------------------------------------------------
1195
- /** Create a flag. */
1196
- async create(key, options) {
1197
- let values = options.values;
1198
- if (values === void 0 && options.type === "BOOLEAN") {
1199
- values = [
1190
+ /** Create an unsaved boolean flag. Call `.save()` to persist. */
1191
+ newBooleanFlag(key, options) {
1192
+ return new BooleanFlag(this, {
1193
+ id: null,
1194
+ key,
1195
+ name: options.name ?? keyToDisplayName(key),
1196
+ type: "BOOLEAN",
1197
+ default: options.default,
1198
+ values: [
1200
1199
  { name: "True", value: true },
1201
1200
  { 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);
1201
+ ],
1202
+ description: options.description ?? null,
1203
+ environments: {},
1204
+ createdAt: null,
1205
+ updatedAt: null
1206
+ });
1207
+ }
1208
+ /** Create an unsaved string flag. Call `.save()` to persist. */
1209
+ newStringFlag(key, options) {
1210
+ return new StringFlag(this, {
1211
+ id: null,
1212
+ key,
1213
+ name: options.name ?? keyToDisplayName(key),
1214
+ type: "STRING",
1215
+ default: options.default,
1216
+ values: options.values ?? [],
1217
+ description: options.description ?? null,
1218
+ environments: {},
1219
+ createdAt: null,
1220
+ updatedAt: null
1221
+ });
1227
1222
  }
1228
- /** Fetch a flag by UUID. */
1229
- async get(flagId) {
1223
+ /** Create an unsaved number flag. Call `.save()` to persist. */
1224
+ newNumberFlag(key, options) {
1225
+ return new NumberFlag(this, {
1226
+ id: null,
1227
+ key,
1228
+ name: options.name ?? keyToDisplayName(key),
1229
+ type: "NUMERIC",
1230
+ default: options.default,
1231
+ values: options.values ?? [],
1232
+ description: options.description ?? null,
1233
+ environments: {},
1234
+ createdAt: null,
1235
+ updatedAt: null
1236
+ });
1237
+ }
1238
+ /** Create an unsaved JSON flag. Call `.save()` to persist. */
1239
+ newJsonFlag(key, options) {
1240
+ return new JsonFlag(this, {
1241
+ id: null,
1242
+ key,
1243
+ name: options.name ?? keyToDisplayName(key),
1244
+ type: "JSON",
1245
+ default: options.default,
1246
+ values: options.values ?? [],
1247
+ description: options.description ?? null,
1248
+ environments: {},
1249
+ createdAt: null,
1250
+ updatedAt: null
1251
+ });
1252
+ }
1253
+ // ------------------------------------------------------------------
1254
+ // Management: CRUD
1255
+ // ------------------------------------------------------------------
1256
+ /** Fetch a flag by key. */
1257
+ async get(key) {
1230
1258
  let data;
1231
1259
  try {
1232
- const result = await this._http.GET("/api/v1/flags/{id}", {
1233
- params: { path: { id: flagId } }
1260
+ const result = await this._http.GET("/api/v1/flags", {
1261
+ params: { query: { "filter[key]": key } }
1234
1262
  });
1235
- if (result.error !== void 0) await checkError2(result.response, `Flag ${flagId} not found`);
1263
+ if (result.error !== void 0)
1264
+ await checkError2(result.response, `Flag with key '${key}' not found`);
1236
1265
  data = result.data;
1237
1266
  } catch (err) {
1238
1267
  wrapFetchError2(err);
1239
1268
  }
1240
- if (!data || !data.data) throw new SmplNotFoundError(`Flag ${flagId} not found`);
1241
- return this._resourceToModel(data.data);
1269
+ if (!data || !data.data || data.data.length === 0) {
1270
+ throw new SmplNotFoundError(`Flag with key '${key}' not found`);
1271
+ }
1272
+ return this._resourceToModel(data.data[0]);
1242
1273
  }
1243
1274
  /** List all flags. */
1244
1275
  async list() {
@@ -1253,161 +1284,148 @@ var FlagsClient = class {
1253
1284
  if (!data) return [];
1254
1285
  return data.data.map((r) => this._resourceToModel(r));
1255
1286
  }
1256
- /** Delete a flag by UUID. */
1257
- async delete(flagId) {
1287
+ /** Delete a flag by key. */
1288
+ async delete(key) {
1289
+ const flag = await this.get(key);
1258
1290
  try {
1259
1291
  const result = await this._http.DELETE("/api/v1/flags/{id}", {
1260
- params: { path: { id: flagId } }
1292
+ params: { path: { id: flag.id } }
1261
1293
  });
1262
1294
  if (result.error !== void 0 && result.response.status !== 204)
1263
- await checkError2(result.response, `Failed to delete flag ${flagId}`);
1295
+ await checkError2(result.response, `Failed to delete flag '${key}'`);
1264
1296
  } catch (err) {
1265
1297
  wrapFetchError2(err);
1266
1298
  }
1267
1299
  }
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;
1300
+ // ------------------------------------------------------------------
1301
+ // Management: internal save methods (called by Flag.save())
1302
+ // ------------------------------------------------------------------
1303
+ /** @internal — POST a new flag. */
1304
+ async _createFlag(flag) {
1275
1305
  const body = {
1276
1306
  data: {
1277
1307
  type: "flag",
1278
1308
  attributes: {
1279
1309
  key: flag.key,
1280
- name: options.name !== void 0 ? options.name : flag.name,
1310
+ name: flag.name,
1311
+ description: flag.description ?? "",
1281
1312
  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 } : {}
1313
+ default: flag.default,
1314
+ values: flag.values,
1315
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1286
1316
  }
1287
1317
  }
1288
1318
  };
1289
1319
  let data;
1290
1320
  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}`);
1321
+ const result = await this._http.POST("/api/v1/flags", { body });
1322
+ if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1297
1323
  data = result.data;
1298
1324
  } catch (err) {
1299
1325
  wrapFetchError2(err);
1300
1326
  }
1301
- if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1327
+ if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1302
1328
  return this._resourceToModel(data.data);
1303
1329
  }
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 } }
1330
+ /** @internal — PUT a flag update. */
1331
+ async _updateFlag(flag) {
1332
+ const body = {
1333
+ data: {
1334
+ type: "flag",
1335
+ attributes: {
1336
+ key: flag.key,
1337
+ name: flag.name,
1338
+ type: flag.type,
1339
+ default: flag.default,
1340
+ values: flag.values,
1341
+ description: flag.description ?? "",
1342
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1314
1343
  }
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) {
1344
+ }
1345
+ };
1327
1346
  let data;
1328
1347
  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
- }
1348
+ const result = await this._http.PUT("/api/v1/flags/{id}", {
1349
+ params: { path: { id: flag.id } },
1350
+ body
1337
1351
  });
1338
1352
  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");
1353
+ await checkError2(result.response, `Failed to update flag ${flag.id}`);
1381
1354
  data = result.data;
1382
1355
  } catch (err) {
1383
1356
  wrapFetchError2(err);
1384
1357
  }
1385
- return data?.data ?? [];
1358
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1359
+ return this._resourceToModel(data.data);
1386
1360
  }
1387
1361
  // ------------------------------------------------------------------
1388
1362
  // Runtime: typed flag handles
1389
1363
  // ------------------------------------------------------------------
1390
- /** Declare a boolean flag handle. */
1391
- boolFlag(key, defaultValue) {
1392
- const handle = new BoolFlagHandle(this, key, defaultValue);
1364
+ /** Declare a boolean flag handle for runtime evaluation. */
1365
+ booleanFlag(key, defaultValue) {
1366
+ const handle = new BooleanFlag(this, {
1367
+ id: null,
1368
+ key,
1369
+ name: key,
1370
+ type: "BOOLEAN",
1371
+ default: defaultValue,
1372
+ values: [],
1373
+ description: null,
1374
+ environments: {},
1375
+ createdAt: null,
1376
+ updatedAt: null
1377
+ });
1393
1378
  this._handles[key] = handle;
1394
1379
  return handle;
1395
1380
  }
1396
- /** Declare a string flag handle. */
1381
+ /** Declare a string flag handle for runtime evaluation. */
1397
1382
  stringFlag(key, defaultValue) {
1398
- const handle = new StringFlagHandle(this, key, defaultValue);
1383
+ const handle = new StringFlag(this, {
1384
+ id: null,
1385
+ key,
1386
+ name: key,
1387
+ type: "STRING",
1388
+ default: defaultValue,
1389
+ values: [],
1390
+ description: null,
1391
+ environments: {},
1392
+ createdAt: null,
1393
+ updatedAt: null
1394
+ });
1399
1395
  this._handles[key] = handle;
1400
1396
  return handle;
1401
1397
  }
1402
- /** Declare a numeric flag handle. */
1398
+ /** Declare a numeric flag handle for runtime evaluation. */
1403
1399
  numberFlag(key, defaultValue) {
1404
- const handle = new NumberFlagHandle(this, key, defaultValue);
1400
+ const handle = new NumberFlag(this, {
1401
+ id: null,
1402
+ key,
1403
+ name: key,
1404
+ type: "NUMERIC",
1405
+ default: defaultValue,
1406
+ values: [],
1407
+ description: null,
1408
+ environments: {},
1409
+ createdAt: null,
1410
+ updatedAt: null
1411
+ });
1405
1412
  this._handles[key] = handle;
1406
1413
  return handle;
1407
1414
  }
1408
- /** Declare a JSON flag handle. */
1415
+ /** Declare a JSON flag handle for runtime evaluation. */
1409
1416
  jsonFlag(key, defaultValue) {
1410
- const handle = new JsonFlagHandle(this, key, defaultValue);
1417
+ const handle = new JsonFlag(this, {
1418
+ id: null,
1419
+ key,
1420
+ name: key,
1421
+ type: "JSON",
1422
+ default: defaultValue,
1423
+ values: [],
1424
+ description: null,
1425
+ environments: {},
1426
+ createdAt: null,
1427
+ updatedAt: null
1428
+ });
1411
1429
  this._handles[key] = handle;
1412
1430
  return handle;
1413
1431
  }
@@ -1417,41 +1435,32 @@ var FlagsClient = class {
1417
1435
  /**
1418
1436
  * Register a context provider function.
1419
1437
  *
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
- * ```
1438
+ * Called on every `handle.get()` to supply the current evaluation context.
1428
1439
  */
1429
1440
  setContextProvider(fn) {
1430
1441
  this._contextProvider = fn;
1431
1442
  }
1432
1443
  /**
1433
1444
  * Register a context provider — decorator-style alias.
1434
- *
1435
- * ```typescript
1436
- * const provider = client.flags.contextProvider(() => [...]);
1437
- * ```
1438
1445
  */
1439
1446
  contextProvider(fn) {
1440
1447
  this._contextProvider = fn;
1441
1448
  return fn;
1442
1449
  }
1443
1450
  // ------------------------------------------------------------------
1444
- // Runtime: connect / disconnect / refresh
1451
+ // Runtime: initialize / disconnect / refresh
1445
1452
  // ------------------------------------------------------------------
1446
1453
  /**
1447
- * Connect to an environment: fetch flag definitions, register on
1448
- * shared WebSocket, enable local evaluation.
1449
- * @internalcalled by SmplClient.connect().
1454
+ * Initialize the flags runtime: fetch definitions and wire WebSocket.
1455
+ *
1456
+ * Idempotentsafe to call multiple times. Must be called (and awaited)
1457
+ * before using `.get()` on flag handles.
1450
1458
  */
1451
- async _connectInternal(environment) {
1452
- this._environment = environment;
1459
+ async initialize() {
1460
+ if (this._initialized) return;
1461
+ this._environment = this._parent?._environment ?? null;
1453
1462
  await this._fetchAllFlags();
1454
- this._connected = true;
1463
+ this._initialized = true;
1455
1464
  this._cache.clear();
1456
1465
  this._wsManager = this._ensureWs();
1457
1466
  this._wsManager.on("flag_changed", this._handleFlagChanged);
@@ -1467,7 +1476,7 @@ var FlagsClient = class {
1467
1476
  await this._flushContexts();
1468
1477
  this._flagStore = {};
1469
1478
  this._cache.clear();
1470
- this._connected = false;
1479
+ this._initialized = false;
1471
1480
  this._environment = null;
1472
1481
  }
1473
1482
  /** Re-fetch all flag definitions and clear cache. */
@@ -1488,22 +1497,27 @@ var FlagsClient = class {
1488
1497
  return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
1489
1498
  }
1490
1499
  // ------------------------------------------------------------------
1491
- // Runtime: change listeners
1500
+ // Runtime: change listeners (dual-mode)
1492
1501
  // ------------------------------------------------------------------
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
1502
  /**
1499
- * Register a global change listener — decorator-style alias.
1503
+ * Register a change listener.
1500
1504
  *
1501
- * ```typescript
1502
- * const listener = client.flags.onChange((event) => { ... });
1503
- * ```
1505
+ * - `onChange(callback)` — fires for any flag change (global).
1506
+ * - `onChange(key, callback)` fires only for the specified flag key.
1504
1507
  */
1505
- onChange(callback) {
1506
- return this.onChangeAny(callback);
1508
+ onChange(callbackOrKey, callback) {
1509
+ if (typeof callbackOrKey === "function") {
1510
+ this._globalListeners.push(callbackOrKey);
1511
+ } else {
1512
+ const key = callbackOrKey;
1513
+ if (!callback) {
1514
+ throw new SmplError("onChange(key, callback) requires a callback function.");
1515
+ }
1516
+ if (!this._keyListeners.has(key)) {
1517
+ this._keyListeners.set(key, []);
1518
+ }
1519
+ this._keyListeners.get(key).push(callback);
1520
+ }
1507
1521
  }
1508
1522
  // ------------------------------------------------------------------
1509
1523
  // Runtime: context registration
@@ -1512,7 +1526,7 @@ var FlagsClient = class {
1512
1526
  * Explicitly register context(s) for background batch registration.
1513
1527
  *
1514
1528
  * Accepts a single Context or an array. Fire-and-forget — never
1515
- * blocks. Works before `connect()` is called.
1529
+ * blocks. Works before `initialize()` is called.
1516
1530
  */
1517
1531
  register(context) {
1518
1532
  if (Array.isArray(context)) {
@@ -1530,8 +1544,6 @@ var FlagsClient = class {
1530
1544
  // ------------------------------------------------------------------
1531
1545
  /**
1532
1546
  * Tier 1 explicit evaluation — stateless, no provider or cache.
1533
- *
1534
- * Useful for scripts, one-off jobs, and infrastructure code.
1535
1547
  */
1536
1548
  async evaluate(key, options) {
1537
1549
  const evalDict = contextsToEvalDict(options.context);
@@ -1539,7 +1551,7 @@ var FlagsClient = class {
1539
1551
  evalDict["service"] = { key: this._parent._service };
1540
1552
  }
1541
1553
  let flagDef = null;
1542
- if (this._connected && key in this._flagStore) {
1554
+ if (this._initialized && key in this._flagStore) {
1543
1555
  flagDef = this._flagStore[key];
1544
1556
  } else {
1545
1557
  const flags = await this._fetchFlagsList();
@@ -1560,8 +1572,8 @@ var FlagsClient = class {
1560
1572
  // ------------------------------------------------------------------
1561
1573
  /** @internal */
1562
1574
  _evaluateHandle(key, defaultValue, context) {
1563
- if (!this._connected) {
1564
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
1575
+ if (!this._initialized) {
1576
+ throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
1565
1577
  }
1566
1578
  let evalDict;
1567
1579
  if (context !== null) {
@@ -1598,6 +1610,19 @@ var FlagsClient = class {
1598
1610
  return value;
1599
1611
  }
1600
1612
  // ------------------------------------------------------------------
1613
+ // Internal: _connectInternal (called by SmplClient for backward compat)
1614
+ // ------------------------------------------------------------------
1615
+ /** @internal — called by SmplClient constructor / lazy init. */
1616
+ async _connectInternal(environment) {
1617
+ this._environment = environment;
1618
+ await this._fetchAllFlags();
1619
+ this._initialized = true;
1620
+ this._cache.clear();
1621
+ this._wsManager = this._ensureWs();
1622
+ this._wsManager.on("flag_changed", this._handleFlagChanged);
1623
+ this._wsManager.on("flag_deleted", this._handleFlagDeleted);
1624
+ }
1625
+ // ------------------------------------------------------------------
1601
1626
  // Internal: event handlers (called by SharedWebSocket)
1602
1627
  // ------------------------------------------------------------------
1603
1628
  _handleFlagChanged = (data) => {
@@ -1649,9 +1674,9 @@ var FlagsClient = class {
1649
1674
  } catch {
1650
1675
  }
1651
1676
  }
1652
- const handle = this._handles[flagKey];
1653
- if (handle) {
1654
- for (const cb of handle._listeners) {
1677
+ const keyCallbacks = this._keyListeners.get(flagKey);
1678
+ if (keyCallbacks) {
1679
+ for (const cb of keyCallbacks) {
1655
1680
  try {
1656
1681
  cb(event);
1657
1682
  } catch {
@@ -1687,10 +1712,11 @@ var FlagsClient = class {
1687
1712
  // ------------------------------------------------------------------
1688
1713
  // Internal: model conversion
1689
1714
  // ------------------------------------------------------------------
1715
+ /** @internal */
1690
1716
  _resourceToModel(resource) {
1691
1717
  const attrs = resource.attributes;
1692
1718
  return new Flag(this, {
1693
- id: resource.id ?? "",
1719
+ id: resource.id ?? null,
1694
1720
  key: attrs.key,
1695
1721
  name: attrs.name,
1696
1722
  type: attrs.type,
@@ -1714,13 +1740,567 @@ var FlagsClient = class {
1714
1740
  environments: attrs.environments ?? {}
1715
1741
  };
1716
1742
  }
1717
- _parseContextType(data) {
1718
- const attrs = data.attributes ?? {};
1719
- return new ContextType({
1720
- id: data.id ?? "",
1743
+ };
1744
+
1745
+ // src/logging/client.ts
1746
+ var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
1747
+
1748
+ // src/logging/models.ts
1749
+ var Logger = class {
1750
+ /** UUID of the logger, or `null` if unsaved. */
1751
+ id;
1752
+ /** Unique key (dot-separated hierarchy). */
1753
+ key;
1754
+ /** Human-readable display name. */
1755
+ name;
1756
+ /** Base log level, or null if inherited. */
1757
+ level;
1758
+ /** UUID of the parent log group, or null. */
1759
+ group;
1760
+ /** Whether this logger is managed by the platform. */
1761
+ managed;
1762
+ /** Observed sources (services that report this logger). */
1763
+ sources;
1764
+ /** Per-environment level overrides. */
1765
+ environments;
1766
+ /** When the logger was created. */
1767
+ createdAt;
1768
+ /** When the logger was last updated. */
1769
+ updatedAt;
1770
+ /** @internal */
1771
+ _client;
1772
+ /** @internal */
1773
+ constructor(client, fields) {
1774
+ this._client = client;
1775
+ this.id = fields.id;
1776
+ this.key = fields.key;
1777
+ this.name = fields.name;
1778
+ this.level = fields.level;
1779
+ this.group = fields.group;
1780
+ this.managed = fields.managed;
1781
+ this.sources = fields.sources;
1782
+ this.environments = fields.environments;
1783
+ this.createdAt = fields.createdAt;
1784
+ this.updatedAt = fields.updatedAt;
1785
+ }
1786
+ /**
1787
+ * Persist this logger to the server.
1788
+ *
1789
+ * POST if `id` is null (new), PUT if `id` is set (update).
1790
+ */
1791
+ async save() {
1792
+ const saved = await this._client._saveLogger(this);
1793
+ this._apply(saved);
1794
+ }
1795
+ /** Set the base log level (sync local mutation). */
1796
+ setLevel(level) {
1797
+ this.level = level;
1798
+ }
1799
+ /** Clear the base log level (sync local mutation). */
1800
+ clearLevel() {
1801
+ this.level = null;
1802
+ }
1803
+ /** Set an environment-specific log level (sync local mutation). */
1804
+ setEnvironmentLevel(env, level) {
1805
+ const envs = { ...this.environments };
1806
+ envs[env] = { ...envs[env] ?? {}, level };
1807
+ this.environments = envs;
1808
+ }
1809
+ /** Clear an environment-specific log level (sync local mutation). */
1810
+ clearEnvironmentLevel(env) {
1811
+ const envs = { ...this.environments };
1812
+ if (envs[env]) {
1813
+ const entry = { ...envs[env] };
1814
+ delete entry.level;
1815
+ envs[env] = entry;
1816
+ this.environments = envs;
1817
+ }
1818
+ }
1819
+ /** Clear all environment-specific log levels (sync local mutation). */
1820
+ clearAllEnvironmentLevels() {
1821
+ this.environments = {};
1822
+ }
1823
+ /** @internal — copy all fields from another Logger instance. */
1824
+ _apply(other) {
1825
+ this.id = other.id;
1826
+ this.key = other.key;
1827
+ this.name = other.name;
1828
+ this.level = other.level;
1829
+ this.group = other.group;
1830
+ this.managed = other.managed;
1831
+ this.sources = other.sources;
1832
+ this.environments = other.environments;
1833
+ this.createdAt = other.createdAt;
1834
+ this.updatedAt = other.updatedAt;
1835
+ }
1836
+ toString() {
1837
+ return `Logger(key=${this.key}, level=${this.level})`;
1838
+ }
1839
+ };
1840
+ var LogGroup = class {
1841
+ /** UUID of the log group, or `null` if unsaved. */
1842
+ id;
1843
+ /** Unique key. */
1844
+ key;
1845
+ /** Human-readable display name. */
1846
+ name;
1847
+ /** Base log level, or null if inherited. */
1848
+ level;
1849
+ /** UUID of the parent log group, or null. */
1850
+ group;
1851
+ /** Per-environment level overrides. */
1852
+ environments;
1853
+ /** When the log group was created. */
1854
+ createdAt;
1855
+ /** When the log group was last updated. */
1856
+ updatedAt;
1857
+ /** @internal */
1858
+ _client;
1859
+ /** @internal */
1860
+ constructor(client, fields) {
1861
+ this._client = client;
1862
+ this.id = fields.id;
1863
+ this.key = fields.key;
1864
+ this.name = fields.name;
1865
+ this.level = fields.level;
1866
+ this.group = fields.group;
1867
+ this.environments = fields.environments;
1868
+ this.createdAt = fields.createdAt;
1869
+ this.updatedAt = fields.updatedAt;
1870
+ }
1871
+ /**
1872
+ * Persist this log group to the server.
1873
+ *
1874
+ * POST if `id` is null (new), PUT if `id` is set (update).
1875
+ */
1876
+ async save() {
1877
+ const saved = await this._client._saveLogGroup(this);
1878
+ this._apply(saved);
1879
+ }
1880
+ /** Set the base log level (sync local mutation). */
1881
+ setLevel(level) {
1882
+ this.level = level;
1883
+ }
1884
+ /** Clear the base log level (sync local mutation). */
1885
+ clearLevel() {
1886
+ this.level = null;
1887
+ }
1888
+ /** Set an environment-specific log level (sync local mutation). */
1889
+ setEnvironmentLevel(env, level) {
1890
+ const envs = { ...this.environments };
1891
+ envs[env] = { ...envs[env] ?? {}, level };
1892
+ this.environments = envs;
1893
+ }
1894
+ /** Clear an environment-specific log level (sync local mutation). */
1895
+ clearEnvironmentLevel(env) {
1896
+ const envs = { ...this.environments };
1897
+ if (envs[env]) {
1898
+ const entry = { ...envs[env] };
1899
+ delete entry.level;
1900
+ envs[env] = entry;
1901
+ this.environments = envs;
1902
+ }
1903
+ }
1904
+ /** Clear all environment-specific log levels (sync local mutation). */
1905
+ clearAllEnvironmentLevels() {
1906
+ this.environments = {};
1907
+ }
1908
+ /** @internal — copy all fields from another LogGroup instance. */
1909
+ _apply(other) {
1910
+ this.id = other.id;
1911
+ this.key = other.key;
1912
+ this.name = other.name;
1913
+ this.level = other.level;
1914
+ this.group = other.group;
1915
+ this.environments = other.environments;
1916
+ this.createdAt = other.createdAt;
1917
+ this.updatedAt = other.updatedAt;
1918
+ }
1919
+ toString() {
1920
+ return `LogGroup(key=${this.key}, level=${this.level})`;
1921
+ }
1922
+ };
1923
+
1924
+ // src/logging/client.ts
1925
+ var LOGGING_BASE_URL = "https://logging.smplkit.com";
1926
+ async function checkError3(response, _context) {
1927
+ const body = await response.text().catch(() => "");
1928
+ throwForStatus(response.status, body);
1929
+ }
1930
+ function wrapFetchError3(err) {
1931
+ if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
1932
+ throw err;
1933
+ }
1934
+ if (err instanceof TypeError) {
1935
+ throw new SmplConnectionError(`Network error: ${err.message}`);
1936
+ }
1937
+ throw new SmplConnectionError(
1938
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1939
+ );
1940
+ }
1941
+ var LoggingClient = class {
1942
+ /** @internal */
1943
+ _apiKey;
1944
+ /** @internal */
1945
+ _baseUrl = LOGGING_BASE_URL;
1946
+ /** @internal */
1947
+ _http;
1948
+ /** @internal — set by SmplClient after construction. */
1949
+ _parent = null;
1950
+ _ensureWs;
1951
+ _wsManager = null;
1952
+ _started = false;
1953
+ _globalListeners = [];
1954
+ _keyListeners = /* @__PURE__ */ new Map();
1955
+ /** @internal */
1956
+ constructor(apiKey, ensureWs, timeout) {
1957
+ this._apiKey = apiKey;
1958
+ this._ensureWs = ensureWs;
1959
+ const ms = timeout ?? 3e4;
1960
+ this._http = (0, import_openapi_fetch3.default)({
1961
+ baseUrl: LOGGING_BASE_URL,
1962
+ headers: {
1963
+ Authorization: `Bearer ${apiKey}`,
1964
+ Accept: "application/json"
1965
+ },
1966
+ fetch: async (request) => {
1967
+ const controller = new AbortController();
1968
+ const timer = setTimeout(() => controller.abort(), ms);
1969
+ try {
1970
+ return await fetch(new Request(request, { signal: controller.signal }));
1971
+ } catch (err) {
1972
+ if (err instanceof DOMException && err.name === "AbortError") {
1973
+ throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1974
+ }
1975
+ throw err;
1976
+ } finally {
1977
+ clearTimeout(timer);
1978
+ }
1979
+ }
1980
+ });
1981
+ }
1982
+ // ------------------------------------------------------------------
1983
+ // Management: Logger factory
1984
+ // ------------------------------------------------------------------
1985
+ /** Create an unsaved logger. Call `.save()` to persist. */
1986
+ new(key, options) {
1987
+ return new Logger(this, {
1988
+ id: null,
1989
+ key,
1990
+ name: options?.name ?? keyToDisplayName(key),
1991
+ level: null,
1992
+ group: null,
1993
+ managed: options?.managed ?? false,
1994
+ sources: [],
1995
+ environments: {},
1996
+ createdAt: null,
1997
+ updatedAt: null
1998
+ });
1999
+ }
2000
+ // ------------------------------------------------------------------
2001
+ // Management: Logger CRUD
2002
+ // ------------------------------------------------------------------
2003
+ /** Fetch a logger by key. */
2004
+ async get(key) {
2005
+ let data;
2006
+ try {
2007
+ const result = await this._http.GET("/api/v1/loggers", {
2008
+ params: { query: { "filter[key]": key } }
2009
+ });
2010
+ if (result.error !== void 0)
2011
+ await checkError3(result.response, `Logger with key '${key}' not found`);
2012
+ data = result.data;
2013
+ } catch (err) {
2014
+ wrapFetchError3(err);
2015
+ }
2016
+ if (!data || !data.data || data.data.length === 0) {
2017
+ throw new SmplNotFoundError(`Logger with key '${key}' not found`);
2018
+ }
2019
+ return this._loggerToModel(data.data[0]);
2020
+ }
2021
+ /** List all loggers. */
2022
+ async list() {
2023
+ let data;
2024
+ try {
2025
+ const result = await this._http.GET("/api/v1/loggers", {});
2026
+ if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
2027
+ data = result.data;
2028
+ } catch (err) {
2029
+ wrapFetchError3(err);
2030
+ }
2031
+ if (!data) return [];
2032
+ return data.data.map((r) => this._loggerToModel(r));
2033
+ }
2034
+ /** Delete a logger by key. */
2035
+ async delete(key) {
2036
+ const logger = await this.get(key);
2037
+ try {
2038
+ const result = await this._http.DELETE("/api/v1/loggers/{id}", {
2039
+ params: { path: { id: logger.id } }
2040
+ });
2041
+ if (result.error !== void 0 && result.response.status !== 204)
2042
+ await checkError3(result.response, `Failed to delete logger '${key}'`);
2043
+ } catch (err) {
2044
+ wrapFetchError3(err);
2045
+ }
2046
+ }
2047
+ // ------------------------------------------------------------------
2048
+ // Management: LogGroup factory
2049
+ // ------------------------------------------------------------------
2050
+ /** Create an unsaved log group. Call `.save()` to persist. */
2051
+ newGroup(key, options) {
2052
+ return new LogGroup(this, {
2053
+ id: null,
2054
+ key,
2055
+ name: options?.name ?? keyToDisplayName(key),
2056
+ level: null,
2057
+ group: options?.group ?? null,
2058
+ environments: {},
2059
+ createdAt: null,
2060
+ updatedAt: null
2061
+ });
2062
+ }
2063
+ // ------------------------------------------------------------------
2064
+ // Management: LogGroup CRUD
2065
+ // ------------------------------------------------------------------
2066
+ /** Fetch a log group by key. */
2067
+ async getGroup(key) {
2068
+ const groups = await this.listGroups();
2069
+ const match = groups.find((g) => g.key === key);
2070
+ if (!match) {
2071
+ throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
2072
+ }
2073
+ return match;
2074
+ }
2075
+ /** List all log groups. */
2076
+ async listGroups() {
2077
+ let data;
2078
+ try {
2079
+ const result = await this._http.GET("/api/v1/log_groups", {});
2080
+ if (result.error !== void 0)
2081
+ await checkError3(result.response, "Failed to list log groups");
2082
+ data = result.data;
2083
+ } catch (err) {
2084
+ wrapFetchError3(err);
2085
+ }
2086
+ if (!data) return [];
2087
+ return data.data.map((r) => this._groupToModel(r));
2088
+ }
2089
+ /** Delete a log group by key. */
2090
+ async deleteGroup(key) {
2091
+ const group = await this.getGroup(key);
2092
+ try {
2093
+ const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
2094
+ params: { path: { id: group.id } }
2095
+ });
2096
+ if (result.error !== void 0 && result.response.status !== 204)
2097
+ await checkError3(result.response, `Failed to delete log group '${key}'`);
2098
+ } catch (err) {
2099
+ wrapFetchError3(err);
2100
+ }
2101
+ }
2102
+ // ------------------------------------------------------------------
2103
+ // Management: internal save methods
2104
+ // ------------------------------------------------------------------
2105
+ /** @internal — POST or PUT a logger. */
2106
+ async _saveLogger(logger) {
2107
+ const body = {
2108
+ data: {
2109
+ type: "logger",
2110
+ attributes: {
2111
+ key: logger.key,
2112
+ name: logger.name,
2113
+ level: logger.level,
2114
+ group: logger.group,
2115
+ managed: logger.managed,
2116
+ environments: logger.environments
2117
+ }
2118
+ }
2119
+ };
2120
+ if (logger.id === null) {
2121
+ let data;
2122
+ try {
2123
+ const result = await this._http.POST("/api/v1/loggers", { body });
2124
+ if (result.error !== void 0)
2125
+ await checkError3(result.response, "Failed to create logger");
2126
+ data = result.data;
2127
+ } catch (err) {
2128
+ wrapFetchError3(err);
2129
+ }
2130
+ if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
2131
+ return this._loggerToModel(data.data);
2132
+ } else {
2133
+ let data;
2134
+ try {
2135
+ const result = await this._http.PUT("/api/v1/loggers/{id}", {
2136
+ params: { path: { id: logger.id } },
2137
+ body
2138
+ });
2139
+ if (result.error !== void 0)
2140
+ await checkError3(result.response, `Failed to update logger ${logger.id}`);
2141
+ data = result.data;
2142
+ } catch (err) {
2143
+ wrapFetchError3(err);
2144
+ }
2145
+ if (!data || !data.data)
2146
+ throw new SmplValidationError(`Failed to update logger ${logger.id}`);
2147
+ return this._loggerToModel(data.data);
2148
+ }
2149
+ }
2150
+ /** @internal — POST or PUT a log group. */
2151
+ async _saveLogGroup(group) {
2152
+ const body = {
2153
+ data: {
2154
+ type: "log_group",
2155
+ attributes: {
2156
+ key: group.key,
2157
+ name: group.name,
2158
+ level: group.level,
2159
+ group: group.group,
2160
+ environments: group.environments
2161
+ }
2162
+ }
2163
+ };
2164
+ if (group.id === null) {
2165
+ let data;
2166
+ try {
2167
+ const result = await this._http.POST("/api/v1/log_groups", { body });
2168
+ if (result.error !== void 0)
2169
+ await checkError3(result.response, "Failed to create log group");
2170
+ data = result.data;
2171
+ } catch (err) {
2172
+ wrapFetchError3(err);
2173
+ }
2174
+ if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
2175
+ return this._groupToModel(data.data);
2176
+ } else {
2177
+ let data;
2178
+ try {
2179
+ const result = await this._http.PUT("/api/v1/log_groups/{id}", {
2180
+ params: { path: { id: group.id } },
2181
+ body
2182
+ });
2183
+ if (result.error !== void 0)
2184
+ await checkError3(result.response, `Failed to update log group ${group.id}`);
2185
+ data = result.data;
2186
+ } catch (err) {
2187
+ wrapFetchError3(err);
2188
+ }
2189
+ if (!data || !data.data)
2190
+ throw new SmplValidationError(`Failed to update log group ${group.id}`);
2191
+ return this._groupToModel(data.data);
2192
+ }
2193
+ }
2194
+ // ------------------------------------------------------------------
2195
+ // Runtime: start (scaffolded)
2196
+ // ------------------------------------------------------------------
2197
+ /**
2198
+ * Start the logging runtime.
2199
+ *
2200
+ * Fetches existing loggers/groups and wires WebSocket listeners for
2201
+ * live updates. Idempotent — safe to call multiple times.
2202
+ *
2203
+ * Note: Node.js auto-discovery (equivalent to Python's logging module
2204
+ * monkey-patching) is deferred. Management methods work without start().
2205
+ */
2206
+ async start() {
2207
+ if (this._started) return;
2208
+ this._wsManager = this._ensureWs();
2209
+ this._wsManager.on("logger_changed", this._handleLoggerChanged);
2210
+ this._started = true;
2211
+ }
2212
+ // ------------------------------------------------------------------
2213
+ // Runtime: change listeners (dual-mode)
2214
+ // ------------------------------------------------------------------
2215
+ /**
2216
+ * Register a change listener.
2217
+ *
2218
+ * - `onChange(callback)` — fires for any logger change (global).
2219
+ * - `onChange(key, callback)` — fires only for the specified logger key.
2220
+ */
2221
+ onChange(callbackOrKey, callback) {
2222
+ if (typeof callbackOrKey === "function") {
2223
+ this._globalListeners.push(callbackOrKey);
2224
+ } else {
2225
+ const key = callbackOrKey;
2226
+ if (!callback) {
2227
+ throw new SmplError("onChange(key, callback) requires a callback function.");
2228
+ }
2229
+ if (!this._keyListeners.has(key)) {
2230
+ this._keyListeners.set(key, []);
2231
+ }
2232
+ this._keyListeners.get(key).push(callback);
2233
+ }
2234
+ }
2235
+ // ------------------------------------------------------------------
2236
+ // Internal: close
2237
+ // ------------------------------------------------------------------
2238
+ /** @internal */
2239
+ _close() {
2240
+ if (this._wsManager !== null) {
2241
+ this._wsManager.off("logger_changed", this._handleLoggerChanged);
2242
+ this._wsManager = null;
2243
+ }
2244
+ this._started = false;
2245
+ }
2246
+ // ------------------------------------------------------------------
2247
+ // Internal: WebSocket handler
2248
+ // ------------------------------------------------------------------
2249
+ _handleLoggerChanged = (data) => {
2250
+ const key = data.key;
2251
+ if (key) {
2252
+ const level = data.level ?? null;
2253
+ const event = {
2254
+ key,
2255
+ level,
2256
+ source: "websocket"
2257
+ };
2258
+ for (const cb of this._globalListeners) {
2259
+ try {
2260
+ cb(event);
2261
+ } catch {
2262
+ }
2263
+ }
2264
+ const keyCallbacks = this._keyListeners.get(key);
2265
+ if (keyCallbacks) {
2266
+ for (const cb of keyCallbacks) {
2267
+ try {
2268
+ cb(event);
2269
+ } catch {
2270
+ }
2271
+ }
2272
+ }
2273
+ }
2274
+ };
2275
+ // ------------------------------------------------------------------
2276
+ // Internal: model conversion
2277
+ // ------------------------------------------------------------------
2278
+ _loggerToModel(resource) {
2279
+ const attrs = resource.attributes;
2280
+ return new Logger(this, {
2281
+ id: resource.id ?? null,
1721
2282
  key: attrs.key ?? "",
1722
- name: attrs.name ?? "",
1723
- attributes: attrs.attributes ?? {}
2283
+ name: attrs.name,
2284
+ level: attrs.level ?? null,
2285
+ group: attrs.group ?? null,
2286
+ managed: attrs.managed ?? false,
2287
+ sources: attrs.sources ?? [],
2288
+ environments: attrs.environments ?? {},
2289
+ createdAt: attrs.created_at ?? null,
2290
+ updatedAt: attrs.updated_at ?? null
2291
+ });
2292
+ }
2293
+ _groupToModel(resource) {
2294
+ const attrs = resource.attributes;
2295
+ return new LogGroup(this, {
2296
+ id: resource.id ?? null,
2297
+ key: attrs.key ?? "",
2298
+ name: attrs.name,
2299
+ level: attrs.level ?? null,
2300
+ group: attrs.group ?? null,
2301
+ environments: attrs.environments ?? {},
2302
+ createdAt: attrs.created_at ?? null,
2303
+ updatedAt: attrs.updated_at ?? null
1724
2304
  });
1725
2305
  }
1726
2306
  };
@@ -1936,17 +2516,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
1936
2516
  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
2517
  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
2518
  var SmplClient = class {
1939
- /** Client for config management-plane operations. */
2519
+ /** Client for config management and runtime. */
1940
2520
  config;
1941
- /** Client for flags management and runtime operations. */
2521
+ /** Client for flags management and runtime. */
1942
2522
  flags;
2523
+ /** Client for logging management and runtime. */
2524
+ logging;
1943
2525
  _wsManager = null;
1944
2526
  _apiKey;
1945
2527
  /** @internal */
1946
2528
  _environment;
1947
2529
  /** @internal */
1948
2530
  _service;
1949
- _connected = false;
1950
2531
  _timeout;
1951
2532
  _appHttp;
1952
2533
  constructor(options = {}) {
@@ -1963,48 +2544,21 @@ var SmplClient = class {
1963
2544
  const apiKey = resolveApiKey(options.apiKey, environment);
1964
2545
  this._apiKey = apiKey;
1965
2546
  this._timeout = options.timeout ?? 3e4;
1966
- const ms = this._timeout;
1967
- this._appHttp = (0, import_openapi_fetch3.default)({
2547
+ this._appHttp = (0, import_openapi_fetch4.default)({
1968
2548
  baseUrl: APP_BASE_URL2,
1969
2549
  headers: {
1970
2550
  Authorization: `Bearer ${apiKey}`,
1971
2551
  Accept: "application/json"
1972
- },
1973
- fetch: async (request) => {
1974
- const controller = new AbortController();
1975
- const timer = setTimeout(() => controller.abort(), ms);
1976
- try {
1977
- return await fetch(new Request(request, { signal: controller.signal }));
1978
- } catch (err) {
1979
- if (err instanceof DOMException && err.name === "AbortError") {
1980
- throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1981
- }
1982
- throw err;
1983
- } finally {
1984
- clearTimeout(timer);
1985
- }
1986
2552
  }
1987
2553
  });
1988
2554
  this.config = new ConfigClient(apiKey, this._timeout);
1989
2555
  this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
2556
+ this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
1990
2557
  this.config._getSharedWs = () => this._ensureWs();
1991
2558
  this.flags._parent = this;
1992
2559
  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;
2560
+ this.logging._parent = this;
2561
+ void this._registerServiceContext();
2008
2562
  }
2009
2563
  /** @internal */
2010
2564
  async _registerServiceContext() {
@@ -2033,6 +2587,7 @@ var SmplClient = class {
2033
2587
  }
2034
2588
  /** Close the shared WebSocket and release resources. */
2035
2589
  close() {
2590
+ this.logging._close();
2036
2591
  if (this._wsManager !== null) {
2037
2592
  this._wsManager.stop();
2038
2593
  this._wsManager = null;
@@ -2104,29 +2659,44 @@ var Rule = class {
2104
2659
  return result;
2105
2660
  }
2106
2661
  };
2662
+
2663
+ // src/logging/types.ts
2664
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2665
+ LogLevel2["TRACE"] = "TRACE";
2666
+ LogLevel2["DEBUG"] = "DEBUG";
2667
+ LogLevel2["INFO"] = "INFO";
2668
+ LogLevel2["WARN"] = "WARN";
2669
+ LogLevel2["ERROR"] = "ERROR";
2670
+ LogLevel2["FATAL"] = "FATAL";
2671
+ LogLevel2["SILENT"] = "SILENT";
2672
+ return LogLevel2;
2673
+ })(LogLevel || {});
2107
2674
  // Annotate the CommonJS export names for ESM import in node:
2108
2675
  0 && (module.exports = {
2109
- BoolFlagHandle,
2676
+ BooleanFlag,
2110
2677
  Config,
2111
2678
  ConfigClient,
2112
2679
  Context,
2113
- ContextType,
2114
2680
  Flag,
2115
2681
  FlagChangeEvent,
2116
2682
  FlagStats,
2117
2683
  FlagsClient,
2118
- JsonFlagHandle,
2119
- NumberFlagHandle,
2684
+ JsonFlag,
2685
+ LiveConfigProxy,
2686
+ LogGroup,
2687
+ LogLevel,
2688
+ Logger,
2689
+ LoggingClient,
2690
+ NumberFlag,
2120
2691
  Rule,
2121
2692
  SharedWebSocket,
2122
2693
  SmplClient,
2123
2694
  SmplConflictError,
2124
2695
  SmplConnectionError,
2125
2696
  SmplError,
2126
- SmplNotConnectedError,
2127
2697
  SmplNotFoundError,
2128
2698
  SmplTimeoutError,
2129
2699
  SmplValidationError,
2130
- StringFlagHandle
2700
+ StringFlag
2131
2701
  });
2132
2702
  //# sourceMappingURL=index.cjs.map