@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/client.ts
2
- import createClient3 from "openapi-fetch";
2
+ import createClient4 from "openapi-fetch";
3
3
 
4
4
  // src/config/client.ts
5
5
  import createClient from "openapi-fetch";
@@ -62,13 +62,6 @@ var SmplConflictError = class extends SmplError {
62
62
  Object.setPrototypeOf(this, new.target.prototype);
63
63
  }
64
64
  };
65
- var SmplNotConnectedError = class extends SmplError {
66
- constructor(message) {
67
- super(message);
68
- this.name = "SmplNotConnectedError";
69
- Object.setPrototypeOf(this, new.target.prototype);
70
- }
71
- };
72
65
  var SmplValidationError = class extends SmplError {
73
66
  constructor(message, statusCode, responseBody, errors) {
74
67
  super(message, statusCode ?? 422, responseBody, errors);
@@ -148,9 +141,9 @@ function resolveChain(chain, environment) {
148
141
 
149
142
  // src/config/types.ts
150
143
  var Config = class {
151
- /** UUID of the config. */
144
+ /** UUID of the config, or `null` if unsaved. */
152
145
  id;
153
- /** Human-readable key (e.g. `"user_service"`). */
146
+ /** Human-readable key (e.g. `"user-service"`). */
154
147
  key;
155
148
  /** Display name. */
156
149
  name;
@@ -170,10 +163,7 @@ var Config = class {
170
163
  createdAt;
171
164
  /** When the config was last updated, or null if unavailable. */
172
165
  updatedAt;
173
- /**
174
- * Internal reference to the parent client.
175
- * @internal
176
- */
166
+ /** @internal */
177
167
  _client;
178
168
  /** @internal */
179
169
  constructor(client, fields) {
@@ -189,100 +179,31 @@ var Config = class {
189
179
  this.updatedAt = fields.updatedAt;
190
180
  }
191
181
  /**
192
- * Update this config's attributes on the server.
193
- *
194
- * Builds the request from current attribute values, overriding with any
195
- * provided options. Updates local attributes in place on success.
196
- *
197
- * @param options.name - New display name.
198
- * @param options.description - New description (pass empty string to clear).
199
- * @param options.items - New base values (replaces entirely).
200
- * @param options.environments - New environments dict (replaces entirely).
201
- */
202
- async update(options) {
203
- const updated = await this._client._updateConfig({
204
- configId: this.id,
205
- name: options.name ?? this.name,
206
- key: this.key,
207
- description: options.description !== void 0 ? options.description : this.description,
208
- parent: this.parent,
209
- items: options.items ?? this.items,
210
- environments: options.environments ?? this.environments
211
- });
212
- this.name = updated.name;
213
- this.description = updated.description;
214
- this.items = updated.items;
215
- this.environments = updated.environments;
216
- this.updatedAt = updated.updatedAt;
217
- }
218
- /**
219
- * Replace base or environment-specific values.
220
- *
221
- * When `environment` is provided, replaces that environment's `values`
222
- * sub-dict (other environments are preserved). When omitted, replaces
223
- * the base `items`.
224
- *
225
- * @param values - The complete set of values to set.
226
- * @param environment - Target environment, or omit for base values.
227
- */
228
- async setValues(values, environment) {
229
- let newItems;
230
- let newEnvs;
231
- if (environment === void 0) {
232
- newItems = values;
233
- newEnvs = this.environments;
234
- } else {
235
- newItems = this.items;
236
- const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
237
- existingEntry.values = values;
238
- newEnvs = { ...this.environments, [environment]: existingEntry };
239
- }
240
- const updated = await this._client._updateConfig({
241
- configId: this.id,
242
- name: this.name,
243
- key: this.key,
244
- description: this.description,
245
- parent: this.parent,
246
- items: newItems,
247
- environments: newEnvs
248
- });
249
- this.items = updated.items;
250
- this.environments = updated.environments;
251
- this.updatedAt = updated.updatedAt;
252
- }
253
- /**
254
- * Set a single key within base or environment-specific values.
182
+ * Persist this config to the server.
255
183
  *
256
- * Merges the key into existing values rather than replacing all values.
257
- *
258
- * @param key - The config key to set.
259
- * @param value - The value to assign.
260
- * @param environment - Target environment, or omit for base values.
184
+ * POST if `id` is null (new config), PUT if `id` is set (update).
185
+ * Updates this instance in-place with the server response.
261
186
  */
262
- async setValue(key, value, environment) {
263
- if (environment === void 0) {
264
- const merged = { ...this.items, [key]: value };
265
- await this.setValues(merged);
187
+ async save() {
188
+ if (this.id === null) {
189
+ const created = await this._client._createConfig(this);
190
+ this._apply(created);
266
191
  } else {
267
- const envEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? this.environments[environment] : {};
268
- const existing = {
269
- ...typeof envEntry.values === "object" && envEntry.values !== null ? envEntry.values : {}
270
- };
271
- existing[key] = value;
272
- await this.setValues(existing, environment);
192
+ const updated = await this._client._updateConfig(this);
193
+ this._apply(updated);
273
194
  }
274
195
  }
275
196
  /**
276
197
  * Walk the parent chain and return config data objects, child-to-root.
277
198
  * @internal
278
199
  */
279
- async _buildChain(_timeout) {
280
- const chain = [{ id: this.id, items: this.items, environments: this.environments }];
200
+ async _buildChain() {
201
+ const chain = [{ id: this.id ?? "", items: this.items, environments: this.environments }];
281
202
  let parentId = this.parent;
282
203
  while (parentId !== null) {
283
- const parentConfig = await this._client.get({ id: parentId });
204
+ const parentConfig = await this._client._getById(parentId);
284
205
  chain.push({
285
- id: parentConfig.id,
206
+ id: parentConfig.id ?? "",
286
207
  items: parentConfig.items,
287
208
  environments: parentConfig.environments
288
209
  });
@@ -290,11 +211,82 @@ var Config = class {
290
211
  }
291
212
  return chain;
292
213
  }
214
+ /** @internal — copy all fields from another Config instance. */
215
+ _apply(other) {
216
+ this.id = other.id;
217
+ this.key = other.key;
218
+ this.name = other.name;
219
+ this.description = other.description;
220
+ this.parent = other.parent;
221
+ this.items = other.items;
222
+ this.environments = other.environments;
223
+ this.createdAt = other.createdAt;
224
+ this.updatedAt = other.updatedAt;
225
+ }
293
226
  toString() {
294
227
  return `Config(id=${this.id}, key=${this.key}, name=${this.name})`;
295
228
  }
296
229
  };
297
230
 
231
+ // src/config/proxy.ts
232
+ var LiveConfigProxy = class {
233
+ /** @internal */
234
+ _client;
235
+ /** @internal */
236
+ _key;
237
+ /** @internal */
238
+ _model;
239
+ constructor(client, key, model) {
240
+ this._client = client;
241
+ this._key = key;
242
+ this._model = model;
243
+ return new Proxy(this, {
244
+ get(target, prop, receiver) {
245
+ if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
246
+ return Reflect.get(target, prop, receiver);
247
+ }
248
+ const values = target._currentValues();
249
+ if (target._model) {
250
+ const instance = new target._model(values);
251
+ return instance[prop];
252
+ }
253
+ return values[prop];
254
+ },
255
+ has(target, prop) {
256
+ if (typeof prop === "symbol") return Reflect.has(target, prop);
257
+ const values = target._currentValues();
258
+ return prop in values;
259
+ },
260
+ ownKeys(target) {
261
+ const values = target._currentValues();
262
+ return Object.keys(values);
263
+ },
264
+ getOwnPropertyDescriptor(target, prop) {
265
+ if (typeof prop === "symbol") return Reflect.getOwnPropertyDescriptor(target, prop);
266
+ const values = target._currentValues();
267
+ if (prop in values) {
268
+ return {
269
+ configurable: true,
270
+ enumerable: true,
271
+ value: values[prop],
272
+ writable: false
273
+ };
274
+ }
275
+ return void 0;
276
+ }
277
+ });
278
+ }
279
+ /** @internal */
280
+ _currentValues() {
281
+ return this._client._getCachedConfig(this._key) ?? {};
282
+ }
283
+ };
284
+
285
+ // src/helpers.ts
286
+ function keyToDisplayName(key) {
287
+ return key.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
288
+ }
289
+
298
290
  // src/config/client.ts
299
291
  var BASE_URL = "https://config.smplkit.com";
300
292
  function extractItemValues(items) {
@@ -324,7 +316,7 @@ function extractEnvironments(environments) {
324
316
  function resourceToConfig(resource, client) {
325
317
  const attrs = resource.attributes;
326
318
  return new Config(client, {
327
- id: resource.id ?? "",
319
+ id: resource.id ?? null,
328
320
  key: attrs.key ?? "",
329
321
  name: attrs.name,
330
322
  description: attrs.description ?? null,
@@ -333,8 +325,8 @@ function resourceToConfig(resource, client) {
333
325
  environments: extractEnvironments(
334
326
  attrs.environments
335
327
  ),
336
- createdAt: attrs.created_at ? new Date(attrs.created_at) : null,
337
- updatedAt: attrs.updated_at ? new Date(attrs.updated_at) : null
328
+ createdAt: attrs.created_at ?? null,
329
+ updatedAt: attrs.updated_at ?? null
338
330
  });
339
331
  }
340
332
  async function checkError(response, _context) {
@@ -401,7 +393,7 @@ function buildRequestBody(options) {
401
393
  };
402
394
  }
403
395
  var ConfigClient = class {
404
- /** @internal — used by Config instances for reconnecting and WebSocket auth. */
396
+ /** @internal */
405
397
  _apiKey;
406
398
  /** @internal */
407
399
  _baseUrl = BASE_URL;
@@ -412,7 +404,9 @@ var ConfigClient = class {
412
404
  /** @internal — set by SmplClient after construction. */
413
405
  _parent = null;
414
406
  _configCache = {};
415
- _connected = false;
407
+ /* v8 ignore next — bookkeeping for future use */
408
+ _configStore = [];
409
+ _initialized = false;
416
410
  _listeners = [];
417
411
  /** @internal */
418
412
  constructor(apiKey, timeout) {
@@ -424,7 +418,6 @@ var ConfigClient = class {
424
418
  Authorization: `Bearer ${apiKey}`,
425
419
  Accept: "application/json"
426
420
  },
427
- // openapi-fetch custom fetch receives a pre-built Request object
428
421
  fetch: async (request) => {
429
422
  const controller = new AbortController();
430
423
  const timer = setTimeout(() => controller.abort(), ms);
@@ -441,23 +434,31 @@ var ConfigClient = class {
441
434
  }
442
435
  });
443
436
  }
444
- /**
445
- * Fetch a single config by key or UUID.
446
- *
447
- * Exactly one of `key` or `id` must be provided.
448
- *
449
- * @throws {SmplNotFoundError} If no matching config exists.
450
- */
451
- async get(options) {
452
- const { key, id } = options;
453
- if (key === void 0 === (id === void 0)) {
454
- throw new Error("Exactly one of 'key' or 'id' must be provided.");
455
- }
456
- return id !== void 0 ? this._getById(id) : this._getByKey(key);
437
+ // ------------------------------------------------------------------
438
+ // Management: factory method
439
+ // ------------------------------------------------------------------
440
+ /** Create an unsaved config. Call `.save()` to persist. */
441
+ new(key, options) {
442
+ return new Config(this, {
443
+ id: null,
444
+ key,
445
+ name: options?.name ?? keyToDisplayName(key),
446
+ description: options?.description ?? null,
447
+ parent: options?.parent ?? null,
448
+ items: {},
449
+ environments: {},
450
+ createdAt: null,
451
+ updatedAt: null
452
+ });
457
453
  }
458
- /**
459
- * List all configs for the account.
460
- */
454
+ // ------------------------------------------------------------------
455
+ // Management: CRUD
456
+ // ------------------------------------------------------------------
457
+ /** Fetch a config by key. */
458
+ async get(key) {
459
+ return this._getByKey(key);
460
+ }
461
+ /** List all configs. */
461
462
  async list() {
462
463
  let data;
463
464
  try {
@@ -470,18 +471,31 @@ var ConfigClient = class {
470
471
  if (!data) return [];
471
472
  return data.data.map((r) => resourceToConfig(r, this));
472
473
  }
473
- /**
474
- * Create a new config.
475
- *
476
- * @throws {SmplValidationError} If the server rejects the request.
477
- */
478
- async create(options) {
474
+ /** Delete a config by key. */
475
+ async delete(key) {
476
+ const config = await this.get(key);
477
+ try {
478
+ const result = await this._http.DELETE("/api/v1/configs/{id}", {
479
+ params: { path: { id: config.id } }
480
+ });
481
+ if (result.error !== void 0 && result.response.status !== 204)
482
+ await checkError(result.response, `Failed to delete config '${key}'`);
483
+ } catch (err) {
484
+ wrapFetchError(err);
485
+ }
486
+ }
487
+ // ------------------------------------------------------------------
488
+ // Management: internal save methods (called by Config.save())
489
+ // ------------------------------------------------------------------
490
+ /** @internal — POST a new config. */
491
+ async _createConfig(config) {
479
492
  const body = buildRequestBody({
480
- name: options.name,
481
- key: options.key,
482
- description: options.description,
483
- parent: options.parent,
484
- items: options.items
493
+ name: config.name,
494
+ key: config.key,
495
+ description: config.description,
496
+ parent: config.parent,
497
+ items: config.items,
498
+ environments: config.environments
485
499
  });
486
500
  let data;
487
501
  try {
@@ -494,127 +508,191 @@ var ConfigClient = class {
494
508
  if (!data || !data.data) throw new SmplValidationError("Failed to create config");
495
509
  return resourceToConfig(data.data, this);
496
510
  }
497
- /**
498
- * Delete a config by UUID.
499
- *
500
- * @throws {SmplNotFoundError} If the config does not exist.
501
- * @throws {SmplConflictError} If the config has child configs.
502
- */
503
- async delete(configId) {
511
+ /** @internal — PUT a config update. */
512
+ async _updateConfig(config) {
513
+ const body = buildRequestBody({
514
+ id: config.id,
515
+ name: config.name,
516
+ key: config.key,
517
+ description: config.description,
518
+ parent: config.parent,
519
+ items: config.items,
520
+ environments: config.environments
521
+ });
522
+ let data;
504
523
  try {
505
- const result = await this._http.DELETE("/api/v1/configs/{id}", {
506
- params: { path: { id: configId } }
524
+ const result = await this._http.PUT("/api/v1/configs/{id}", {
525
+ params: { path: { id: config.id } },
526
+ body
507
527
  });
508
- if (result.error !== void 0 && result.response.status !== 204)
509
- await checkError(result.response, `Failed to delete config ${configId}`);
528
+ if (result.error !== void 0)
529
+ await checkError(result.response, `Failed to update config ${config.id}`);
530
+ data = result.data;
510
531
  } catch (err) {
511
532
  wrapFetchError(err);
512
533
  }
534
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
535
+ return resourceToConfig(data.data, this);
513
536
  }
514
- /**
515
- * Fetch all configs, resolve values for the environment, and cache.
516
- * @internal — called by SmplClient.connect().
517
- */
518
- async _connectInternal(environment) {
519
- const configs = await this.list();
520
- const cache = {};
521
- for (const cfg of configs) {
522
- const chain = await cfg._buildChain(this._http);
523
- cache[cfg.key] = resolveChain(chain, environment);
537
+ /** @internal — fetch a config by UUID. */
538
+ async _getById(configId) {
539
+ let data;
540
+ try {
541
+ const result = await this._http.GET("/api/v1/configs/{id}", {
542
+ params: { path: { id: configId } }
543
+ });
544
+ if (result.error !== void 0)
545
+ await checkError(result.response, `Config ${configId} not found`);
546
+ data = result.data;
547
+ } catch (err) {
548
+ wrapFetchError(err);
524
549
  }
525
- this._configCache = cache;
526
- this._connected = true;
550
+ if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
551
+ return resourceToConfig(data.data, this);
527
552
  }
553
+ // ------------------------------------------------------------------
554
+ // Runtime: resolve and subscribe
555
+ // ------------------------------------------------------------------
528
556
  /**
529
- * Read a resolved config value (prescriptive access).
557
+ * Resolve a config's values for the current environment.
530
558
  *
531
- * Requires {@link SmplClient.connect} to have been called.
559
+ * Returns a flat dict of resolved key-value pairs, walking the
560
+ * parent chain and applying environment overrides.
532
561
  *
533
- * @param configKey - The config key to look up.
534
- * @param itemKey - Optional specific item key. If omitted, returns all values.
535
- * @param defaultValue - Default value if the key is missing.
536
- *
537
- * @throws {SmplNotConnectedError} If connect() has not been called.
562
+ * Optionally pass a model class to map the resolved values.
538
563
  */
539
- getValue(configKey, itemKey, defaultValue) {
540
- if (!this._connected) {
541
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
542
- }
543
- const resolved = this._configCache[configKey];
544
- if (resolved === void 0) {
545
- return defaultValue ?? null;
564
+ async resolve(key, model) {
565
+ await this._ensureInitialized();
566
+ const values = this._configCache[key];
567
+ if (values === void 0) {
568
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
546
569
  }
547
- if (itemKey === void 0) {
548
- return { ...resolved };
570
+ if (model) {
571
+ return new model(values);
549
572
  }
550
- return itemKey in resolved ? resolved[itemKey] : defaultValue ?? null;
551
- }
552
- /**
553
- * Return a config value as a string, or `defaultValue` if absent or not a string.
554
- *
555
- * @throws {SmplNotConnectedError} If connect() has not been called.
556
- */
557
- getString(configKey, itemKey, defaultValue = null) {
558
- const value = this.getValue(configKey, itemKey);
559
- return typeof value === "string" ? value : defaultValue;
573
+ return values;
560
574
  }
561
575
  /**
562
- * Return a config value as a number, or `defaultValue` if absent or not a number.
576
+ * Subscribe to a config's values returns a live proxy that
577
+ * auto-updates when the underlying config changes.
563
578
  *
564
- * @throws {SmplNotConnectedError} If connect() has not been called.
579
+ * Optionally pass a model class to map the resolved values.
565
580
  */
566
- getInt(configKey, itemKey, defaultValue = null) {
567
- const value = this.getValue(configKey, itemKey);
568
- return typeof value === "number" ? value : defaultValue;
581
+ async subscribe(key, model) {
582
+ await this._ensureInitialized();
583
+ if (!(key in this._configCache)) {
584
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
585
+ }
586
+ return new LiveConfigProxy(this, key, model);
569
587
  }
588
+ // ------------------------------------------------------------------
589
+ // Runtime: change listeners (3-level overloads)
590
+ // ------------------------------------------------------------------
570
591
  /**
571
- * Return a config value as a boolean, or `defaultValue` if absent or not a boolean.
592
+ * Register a change listener.
572
593
  *
573
- * @throws {SmplNotConnectedError} If connect() has not been called.
594
+ * - `onChange(callback)` fires for any config change (global).
595
+ * - `onChange(configKey, callback)` — fires for changes to a specific config.
596
+ * - `onChange(configKey, itemKey, callback)` — fires for a specific item.
574
597
  */
575
- getBool(configKey, itemKey, defaultValue = null) {
576
- const value = this.getValue(configKey, itemKey);
577
- return typeof value === "boolean" ? value : defaultValue;
598
+ onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
599
+ if (typeof callbackOrConfigKey === "function") {
600
+ this._listeners.push({
601
+ callback: callbackOrConfigKey,
602
+ configKey: null,
603
+ itemKey: null
604
+ });
605
+ } else if (typeof callbackOrItemKey === "function") {
606
+ this._listeners.push({
607
+ callback: callbackOrItemKey,
608
+ configKey: callbackOrConfigKey,
609
+ itemKey: null
610
+ });
611
+ } else if (typeof callbackOrItemKey === "string" && callback) {
612
+ this._listeners.push({
613
+ callback,
614
+ configKey: callbackOrConfigKey,
615
+ itemKey: callbackOrItemKey
616
+ });
617
+ }
578
618
  }
619
+ // ------------------------------------------------------------------
620
+ // Runtime: refresh
621
+ // ------------------------------------------------------------------
579
622
  /**
580
623
  * Re-fetch all configs, re-resolve values, and update the cache.
581
- *
582
- * Fires change listeners for any values that differ from the previous cache.
583
- *
584
- * @throws {SmplNotConnectedError} If connect() has not been called.
624
+ * Fires change listeners for any values that differ.
585
625
  */
586
626
  async refresh() {
587
- if (!this._connected) {
588
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
627
+ if (!this._initialized) {
628
+ throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
589
629
  }
590
630
  const environment = this._parent?._environment;
591
631
  if (!environment) {
592
632
  throw new SmplError("No environment set.");
593
633
  }
594
634
  const configs = await this.list();
635
+ this._configStore = configs;
595
636
  const newCache = {};
596
637
  for (const cfg of configs) {
597
- const chain = await cfg._buildChain(this._http);
638
+ const chain = await cfg._buildChain();
598
639
  newCache[cfg.key] = resolveChain(chain, environment);
599
640
  }
600
641
  const oldCache = this._configCache;
601
642
  this._configCache = newCache;
602
643
  this._diffAndFire(oldCache, newCache, "manual");
603
644
  }
604
- /**
605
- * Register a listener that fires when a config value changes (on refresh).
606
- *
607
- * @param callback - Called with a {@link ConfigChangeEvent} on each change.
608
- * @param options.configKey - If provided, only fire for changes to this config.
609
- * @param options.itemKey - If provided, only fire for changes to this item key.
610
- */
611
- onChange(callback, options) {
612
- this._listeners.push({
613
- callback,
614
- configKey: options?.configKey ?? null,
615
- itemKey: options?.itemKey ?? null
616
- });
645
+ // ------------------------------------------------------------------
646
+ // Runtime: lazy initialization
647
+ // ------------------------------------------------------------------
648
+ /** @internal */
649
+ async _ensureInitialized() {
650
+ if (this._initialized) return;
651
+ const environment = this._parent?._environment;
652
+ if (!environment) {
653
+ throw new SmplError("No environment set. Ensure SmplClient is configured.");
654
+ }
655
+ const configs = await this.list();
656
+ this._configStore = configs;
657
+ const cache = {};
658
+ for (const cfg of configs) {
659
+ const chain = await cfg._buildChain();
660
+ cache[cfg.key] = resolveChain(chain, environment);
661
+ }
662
+ this._configCache = cache;
663
+ this._initialized = true;
664
+ if (this._getSharedWs) {
665
+ const ws = this._getSharedWs();
666
+ ws.on("config_changed", this._handleConfigChanged);
667
+ }
668
+ }
669
+ /** @internal — called by SmplClient for backward compat. */
670
+ async _connectInternal(environment) {
671
+ if (this._initialized) return;
672
+ const configs = await this.list();
673
+ this._configStore = configs;
674
+ const cache = {};
675
+ for (const cfg of configs) {
676
+ const chain = await cfg._buildChain();
677
+ cache[cfg.key] = resolveChain(chain, environment);
678
+ }
679
+ this._configCache = cache;
680
+ this._initialized = true;
617
681
  }
682
+ /** @internal — get resolved config from cache. Used by LiveConfigProxy. */
683
+ _getCachedConfig(key) {
684
+ return this._configCache[key];
685
+ }
686
+ // ------------------------------------------------------------------
687
+ // Internal: WebSocket handler
688
+ // ------------------------------------------------------------------
689
+ _handleConfigChanged = (_data) => {
690
+ void this.refresh().catch(() => {
691
+ });
692
+ };
693
+ // ------------------------------------------------------------------
694
+ // Internal: change detection
695
+ // ------------------------------------------------------------------
618
696
  /** @internal */
619
697
  _diffAndFire(oldCache, newCache, source) {
620
698
  const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
@@ -645,54 +723,9 @@ var ConfigClient = class {
645
723
  }
646
724
  }
647
725
  }
648
- /**
649
- * Internal: PUT a full config update and return the updated model.
650
- *
651
- * Called by {@link Config} instance methods.
652
- * @internal
653
- */
654
- async _updateConfig(payload) {
655
- const body = buildRequestBody({
656
- id: payload.configId,
657
- name: payload.name,
658
- key: payload.key,
659
- description: payload.description,
660
- parent: payload.parent,
661
- items: payload.items,
662
- environments: payload.environments
663
- });
664
- let data;
665
- try {
666
- const result = await this._http.PUT("/api/v1/configs/{id}", {
667
- params: { path: { id: payload.configId } },
668
- body
669
- });
670
- if (result.error !== void 0)
671
- await checkError(result.response, `Failed to update config ${payload.configId}`);
672
- data = result.data;
673
- } catch (err) {
674
- wrapFetchError(err);
675
- }
676
- if (!data || !data.data)
677
- throw new SmplValidationError(`Failed to update config ${payload.configId}`);
678
- return resourceToConfig(data.data, this);
679
- }
680
- // ---- Private helpers ----
681
- async _getById(configId) {
682
- let data;
683
- try {
684
- const result = await this._http.GET("/api/v1/configs/{id}", {
685
- params: { path: { id: configId } }
686
- });
687
- if (result.error !== void 0)
688
- await checkError(result.response, `Config ${configId} not found`);
689
- data = result.data;
690
- } catch (err) {
691
- wrapFetchError(err);
692
- }
693
- if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
694
- return resourceToConfig(data.data, this);
695
- }
726
+ // ------------------------------------------------------------------
727
+ // Internal: fetch by key
728
+ // ------------------------------------------------------------------
696
729
  async _getByKey(key) {
697
730
  let data;
698
731
  try {
@@ -717,7 +750,7 @@ import createClient2 from "openapi-fetch";
717
750
 
718
751
  // src/flags/models.ts
719
752
  var Flag = class {
720
- /** UUID of the flag. */
753
+ /** UUID of the flag, or `null` if unsaved. */
721
754
  id;
722
755
  /** Unique key within the account. */
723
756
  key;
@@ -754,37 +787,35 @@ var Flag = class {
754
787
  this.updatedAt = fields.updatedAt;
755
788
  }
756
789
  /**
757
- * Update this flag's attributes on the server.
790
+ * Persist this flag to the server.
758
791
  *
759
- * Only provided fields are changed; others retain their current values.
792
+ * POST if `id` is null (new flag), PUT if `id` is set (update).
793
+ * Updates this instance in-place with the server response.
760
794
  */
761
- async update(options) {
762
- const updated = await this._client._updateFlag({
763
- flag: this,
764
- environments: options.environments,
765
- values: options.values,
766
- default: options.default,
767
- description: options.description,
768
- name: options.name
769
- });
770
- this._apply(updated);
795
+ async save() {
796
+ if (this.id === null) {
797
+ const created = await this._client._createFlag(this);
798
+ this._apply(created);
799
+ } else {
800
+ const updated = await this._client._updateFlag(this);
801
+ this._apply(updated);
802
+ }
771
803
  }
772
804
  /**
773
- * Add a rule to a specific environment.
805
+ * Add a rule to a specific environment (sync local mutation).
774
806
  *
775
807
  * The built rule must include an `environment` key (set via
776
- * `Rule(...).environment("env_key")`). Re-fetches current state
777
- * first to avoid stale data.
808
+ * `Rule(...).environment("env_key")`). No HTTP call is made.
809
+ *
810
+ * @returns `this` for chaining.
778
811
  */
779
- async addRule(builtRule) {
812
+ addRule(builtRule) {
780
813
  const envKey = builtRule.environment;
781
814
  if (!envKey) {
782
815
  throw new Error(
783
816
  `Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
784
817
  );
785
818
  }
786
- const current = await this._client.get(this.id);
787
- this._apply(current);
788
819
  const envs = { ...this.environments };
789
820
  const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
790
821
  const rules = [...envData.rules ?? []];
@@ -792,13 +823,44 @@ var Flag = class {
792
823
  rules.push(ruleCopy);
793
824
  envData.rules = rules;
794
825
  envs[envKey] = envData;
795
- const updated = await this._client._updateFlag({
796
- flag: this,
797
- environments: envs
798
- });
799
- this._apply(updated);
826
+ this.environments = envs;
827
+ return this;
800
828
  }
801
- /** @internal */
829
+ /** Enable or disable a flag in a specific environment (sync local mutation). */
830
+ setEnvironmentEnabled(envKey, enabled) {
831
+ const envs = { ...this.environments };
832
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
833
+ envData.enabled = enabled;
834
+ envs[envKey] = envData;
835
+ this.environments = envs;
836
+ }
837
+ /** Set the default value for a specific environment (sync local mutation). */
838
+ setEnvironmentDefault(envKey, defaultValue) {
839
+ const envs = { ...this.environments };
840
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
841
+ envData.default = defaultValue;
842
+ envs[envKey] = envData;
843
+ this.environments = envs;
844
+ }
845
+ /** Clear all rules for a specific environment (sync local mutation). */
846
+ clearRules(envKey) {
847
+ const envs = { ...this.environments };
848
+ const envData = envs[envKey];
849
+ if (envData) {
850
+ envs[envKey] = { ...envData, rules: [] };
851
+ this.environments = envs;
852
+ }
853
+ }
854
+ /**
855
+ * Evaluate the flag locally (sync, no HTTP).
856
+ *
857
+ * Requires `initialize()` to have been called.
858
+ */
859
+ /* v8 ignore next 3 — overridden by all exported subclasses */
860
+ get(options) {
861
+ return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
862
+ }
863
+ /** @internal — copy all fields from another Flag instance. */
802
864
  _apply(other) {
803
865
  this.id = other.id;
804
866
  this.key = other.key;
@@ -815,35 +877,52 @@ var Flag = class {
815
877
  return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
816
878
  }
817
879
  };
818
- var ContextType = class {
819
- /** UUID. */
820
- id;
821
- /** Unique key within the account. */
822
- key;
823
- /** Human-readable display name. */
824
- name;
825
- /** Known attributes. */
826
- attributes;
827
- constructor(fields) {
828
- this.id = fields.id;
829
- this.key = fields.key;
830
- this.name = fields.name;
831
- this.attributes = fields.attributes;
880
+ var BooleanFlag = class extends Flag {
881
+ get(options) {
882
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
883
+ if (typeof value === "boolean") {
884
+ return value;
885
+ }
886
+ return this.default;
832
887
  }
833
- toString() {
834
- return `ContextType(key=${this.key}, name=${this.name})`;
888
+ };
889
+ var StringFlag = class extends Flag {
890
+ get(options) {
891
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
892
+ if (typeof value === "string") {
893
+ return value;
894
+ }
895
+ return this.default;
835
896
  }
836
897
  };
837
-
838
- // src/flags/client.ts
839
- import jsonLogic from "json-logic-js";
840
- var FLAGS_BASE_URL = "https://flags.smplkit.com";
841
- var APP_BASE_URL = "https://app.smplkit.com";
842
- var CACHE_MAX_SIZE = 1e4;
843
- var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
844
- var CONTEXT_BATCH_FLUSH_SIZE = 100;
845
- async function checkError2(response, _context) {
846
- const body = await response.text().catch(() => "");
898
+ var NumberFlag = class extends Flag {
899
+ get(options) {
900
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
901
+ if (typeof value === "number") {
902
+ return value;
903
+ }
904
+ return this.default;
905
+ }
906
+ };
907
+ var JsonFlag = class extends Flag {
908
+ get(options) {
909
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
910
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
911
+ return value;
912
+ }
913
+ return this.default;
914
+ }
915
+ };
916
+
917
+ // src/flags/client.ts
918
+ import jsonLogic from "json-logic-js";
919
+ var FLAGS_BASE_URL = "https://flags.smplkit.com";
920
+ var APP_BASE_URL = "https://app.smplkit.com";
921
+ var CACHE_MAX_SIZE = 1e4;
922
+ var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
923
+ var CONTEXT_BATCH_FLUSH_SIZE = 100;
924
+ async function checkError2(response, _context) {
925
+ const body = await response.text().catch(() => "");
847
926
  throwForStatus(response.status, body);
848
927
  }
849
928
  function wrapFetchError2(err) {
@@ -960,88 +1039,6 @@ var FlagStats = class {
960
1039
  this.cacheMisses = cacheMisses;
961
1040
  }
962
1041
  };
963
- var FlagHandleBase = class {
964
- /** @internal */
965
- _namespace;
966
- /** @internal */
967
- _key;
968
- /** @internal */
969
- _default;
970
- /** @internal */
971
- _listeners = [];
972
- constructor(namespace, key, defaultValue) {
973
- this._namespace = namespace;
974
- this._key = key;
975
- this._default = defaultValue;
976
- }
977
- get key() {
978
- return this._key;
979
- }
980
- get default() {
981
- return this._default;
982
- }
983
- /* v8 ignore next 3 — overridden by all exported subclasses */
984
- get(options) {
985
- return this._namespace._evaluateHandle(this._key, this._default, options?.context ?? null);
986
- }
987
- /** Register a flag-specific change listener. Works as a decorator. */
988
- onChange(callback) {
989
- this._listeners.push(callback);
990
- return callback;
991
- }
992
- };
993
- var BoolFlagHandle = class extends FlagHandleBase {
994
- get(options) {
995
- const value = this._namespace._evaluateHandle(
996
- this._key,
997
- this._default,
998
- options?.context ?? null
999
- );
1000
- if (typeof value === "boolean") {
1001
- return value;
1002
- }
1003
- return this._default;
1004
- }
1005
- };
1006
- var StringFlagHandle = class extends FlagHandleBase {
1007
- get(options) {
1008
- const value = this._namespace._evaluateHandle(
1009
- this._key,
1010
- this._default,
1011
- options?.context ?? null
1012
- );
1013
- if (typeof value === "string") {
1014
- return value;
1015
- }
1016
- return this._default;
1017
- }
1018
- };
1019
- var NumberFlagHandle = class extends FlagHandleBase {
1020
- get(options) {
1021
- const value = this._namespace._evaluateHandle(
1022
- this._key,
1023
- this._default,
1024
- options?.context ?? null
1025
- );
1026
- if (typeof value === "number") {
1027
- return value;
1028
- }
1029
- return this._default;
1030
- }
1031
- };
1032
- var JsonFlagHandle = class extends FlagHandleBase {
1033
- get(options) {
1034
- const value = this._namespace._evaluateHandle(
1035
- this._key,
1036
- this._default,
1037
- options?.context ?? null
1038
- );
1039
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1040
- return value;
1041
- }
1042
- return this._default;
1043
- }
1044
- };
1045
1042
  var ContextRegistrationBuffer = class {
1046
1043
  _seen = /* @__PURE__ */ new Map();
1047
1044
  _pending = [];
@@ -1085,13 +1082,14 @@ var FlagsClient = class {
1085
1082
  // Runtime state
1086
1083
  _environment = null;
1087
1084
  _flagStore = {};
1088
- _connected = false;
1085
+ _initialized = false;
1089
1086
  _cache = new ResolutionCache();
1090
1087
  _contextProvider = null;
1091
1088
  _contextBuffer = new ContextRegistrationBuffer();
1092
1089
  _handles = {};
1093
1090
  _globalListeners = [];
1094
- // Shared WebSocket (set during connect)
1091
+ _keyListeners = /* @__PURE__ */ new Map();
1092
+ // Shared WebSocket (set during initialize)
1095
1093
  _wsManager = null;
1096
1094
  _ensureWs;
1097
1095
  /** @internal — set by SmplClient after construction. */
@@ -1133,55 +1131,91 @@ var FlagsClient = class {
1133
1131
  });
1134
1132
  }
1135
1133
  // ------------------------------------------------------------------
1136
- // Management methods
1134
+ // Management: factory methods (return unsaved flags)
1137
1135
  // ------------------------------------------------------------------
1138
- /** Create a flag. */
1139
- async create(key, options) {
1140
- let values = options.values;
1141
- if (values === void 0 && options.type === "BOOLEAN") {
1142
- values = [
1136
+ /** Create an unsaved boolean flag. Call `.save()` to persist. */
1137
+ newBooleanFlag(key, options) {
1138
+ return new BooleanFlag(this, {
1139
+ id: null,
1140
+ key,
1141
+ name: options.name ?? keyToDisplayName(key),
1142
+ type: "BOOLEAN",
1143
+ default: options.default,
1144
+ values: [
1143
1145
  { name: "True", value: true },
1144
1146
  { name: "False", value: false }
1145
- ];
1146
- }
1147
- const body = {
1148
- data: {
1149
- type: "flag",
1150
- attributes: {
1151
- key,
1152
- name: options.name,
1153
- description: options.description ?? "",
1154
- type: options.type,
1155
- default: options.default,
1156
- values: values ?? []
1157
- }
1158
- }
1159
- };
1160
- let data;
1161
- try {
1162
- const result = await this._http.POST("/api/v1/flags", { body });
1163
- if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1164
- data = result.data;
1165
- } catch (err) {
1166
- wrapFetchError2(err);
1167
- }
1168
- if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1169
- return this._resourceToModel(data.data);
1147
+ ],
1148
+ description: options.description ?? null,
1149
+ environments: {},
1150
+ createdAt: null,
1151
+ updatedAt: null
1152
+ });
1153
+ }
1154
+ /** Create an unsaved string flag. Call `.save()` to persist. */
1155
+ newStringFlag(key, options) {
1156
+ return new StringFlag(this, {
1157
+ id: null,
1158
+ key,
1159
+ name: options.name ?? keyToDisplayName(key),
1160
+ type: "STRING",
1161
+ default: options.default,
1162
+ values: options.values ?? [],
1163
+ description: options.description ?? null,
1164
+ environments: {},
1165
+ createdAt: null,
1166
+ updatedAt: null
1167
+ });
1168
+ }
1169
+ /** Create an unsaved number flag. Call `.save()` to persist. */
1170
+ newNumberFlag(key, options) {
1171
+ return new NumberFlag(this, {
1172
+ id: null,
1173
+ key,
1174
+ name: options.name ?? keyToDisplayName(key),
1175
+ type: "NUMERIC",
1176
+ default: options.default,
1177
+ values: options.values ?? [],
1178
+ description: options.description ?? null,
1179
+ environments: {},
1180
+ createdAt: null,
1181
+ updatedAt: null
1182
+ });
1183
+ }
1184
+ /** Create an unsaved JSON flag. Call `.save()` to persist. */
1185
+ newJsonFlag(key, options) {
1186
+ return new JsonFlag(this, {
1187
+ id: null,
1188
+ key,
1189
+ name: options.name ?? keyToDisplayName(key),
1190
+ type: "JSON",
1191
+ default: options.default,
1192
+ values: options.values ?? [],
1193
+ description: options.description ?? null,
1194
+ environments: {},
1195
+ createdAt: null,
1196
+ updatedAt: null
1197
+ });
1170
1198
  }
1171
- /** Fetch a flag by UUID. */
1172
- async get(flagId) {
1199
+ // ------------------------------------------------------------------
1200
+ // Management: CRUD
1201
+ // ------------------------------------------------------------------
1202
+ /** Fetch a flag by key. */
1203
+ async get(key) {
1173
1204
  let data;
1174
1205
  try {
1175
- const result = await this._http.GET("/api/v1/flags/{id}", {
1176
- params: { path: { id: flagId } }
1206
+ const result = await this._http.GET("/api/v1/flags", {
1207
+ params: { query: { "filter[key]": key } }
1177
1208
  });
1178
- if (result.error !== void 0) await checkError2(result.response, `Flag ${flagId} not found`);
1209
+ if (result.error !== void 0)
1210
+ await checkError2(result.response, `Flag with key '${key}' not found`);
1179
1211
  data = result.data;
1180
1212
  } catch (err) {
1181
1213
  wrapFetchError2(err);
1182
1214
  }
1183
- if (!data || !data.data) throw new SmplNotFoundError(`Flag ${flagId} not found`);
1184
- return this._resourceToModel(data.data);
1215
+ if (!data || !data.data || data.data.length === 0) {
1216
+ throw new SmplNotFoundError(`Flag with key '${key}' not found`);
1217
+ }
1218
+ return this._resourceToModel(data.data[0]);
1185
1219
  }
1186
1220
  /** List all flags. */
1187
1221
  async list() {
@@ -1196,161 +1230,148 @@ var FlagsClient = class {
1196
1230
  if (!data) return [];
1197
1231
  return data.data.map((r) => this._resourceToModel(r));
1198
1232
  }
1199
- /** Delete a flag by UUID. */
1200
- async delete(flagId) {
1233
+ /** Delete a flag by key. */
1234
+ async delete(key) {
1235
+ const flag = await this.get(key);
1201
1236
  try {
1202
1237
  const result = await this._http.DELETE("/api/v1/flags/{id}", {
1203
- params: { path: { id: flagId } }
1238
+ params: { path: { id: flag.id } }
1204
1239
  });
1205
1240
  if (result.error !== void 0 && result.response.status !== 204)
1206
- await checkError2(result.response, `Failed to delete flag ${flagId}`);
1241
+ await checkError2(result.response, `Failed to delete flag '${key}'`);
1207
1242
  } catch (err) {
1208
1243
  wrapFetchError2(err);
1209
1244
  }
1210
1245
  }
1211
- /**
1212
- * Internal: PUT a full flag update.
1213
- * Called by {@link Flag} instance methods.
1214
- * @internal
1215
- */
1216
- async _updateFlag(options) {
1217
- const { flag } = options;
1246
+ // ------------------------------------------------------------------
1247
+ // Management: internal save methods (called by Flag.save())
1248
+ // ------------------------------------------------------------------
1249
+ /** @internal — POST a new flag. */
1250
+ async _createFlag(flag) {
1218
1251
  const body = {
1219
1252
  data: {
1220
1253
  type: "flag",
1221
1254
  attributes: {
1222
1255
  key: flag.key,
1223
- name: options.name !== void 0 ? options.name : flag.name,
1256
+ name: flag.name,
1257
+ description: flag.description ?? "",
1224
1258
  type: flag.type,
1225
- default: options.default !== void 0 ? options.default : flag.default,
1226
- values: options.values !== void 0 ? options.values : flag.values,
1227
- description: options.description !== void 0 ? options.description : flag.description ?? "",
1228
- ...options.environments !== void 0 ? { environments: options.environments } : flag.environments && Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1259
+ default: flag.default,
1260
+ values: flag.values,
1261
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1229
1262
  }
1230
1263
  }
1231
1264
  };
1232
1265
  let data;
1233
1266
  try {
1234
- const result = await this._http.PUT("/api/v1/flags/{id}", {
1235
- params: { path: { id: flag.id } },
1236
- body
1237
- });
1238
- if (result.error !== void 0)
1239
- await checkError2(result.response, `Failed to update flag ${flag.id}`);
1267
+ const result = await this._http.POST("/api/v1/flags", { body });
1268
+ if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1240
1269
  data = result.data;
1241
1270
  } catch (err) {
1242
1271
  wrapFetchError2(err);
1243
1272
  }
1244
- if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1273
+ if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1245
1274
  return this._resourceToModel(data.data);
1246
1275
  }
1247
- // ------------------------------------------------------------------
1248
- // Context type management (via generated app client)
1249
- // ------------------------------------------------------------------
1250
- /** Create a context type. */
1251
- async createContextType(key, options) {
1252
- let data;
1253
- try {
1254
- const result = await this._appHttp.POST("/api/v1/context_types", {
1255
- body: {
1256
- data: { type: "context_type", attributes: { key, name: options.name } }
1276
+ /** @internal — PUT a flag update. */
1277
+ async _updateFlag(flag) {
1278
+ const body = {
1279
+ data: {
1280
+ type: "flag",
1281
+ attributes: {
1282
+ key: flag.key,
1283
+ name: flag.name,
1284
+ type: flag.type,
1285
+ default: flag.default,
1286
+ values: flag.values,
1287
+ description: flag.description ?? "",
1288
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1257
1289
  }
1258
- });
1259
- if (result.error !== void 0)
1260
- await checkError2(result.response, "Failed to create context type");
1261
- data = result.data;
1262
- } catch (err) {
1263
- wrapFetchError2(err);
1264
- }
1265
- if (!data || !data.data) throw new SmplValidationError("Failed to create context type");
1266
- return this._parseContextType(data.data);
1267
- }
1268
- /** Update a context type (merge attributes). */
1269
- async updateContextType(ctId, options) {
1290
+ }
1291
+ };
1270
1292
  let data;
1271
1293
  try {
1272
- const result = await this._appHttp.PUT("/api/v1/context_types/{id}", {
1273
- params: { path: { id: ctId } },
1274
- body: {
1275
- data: {
1276
- type: "context_type",
1277
- attributes: { key: options.key, name: options.name, attributes: options.attributes }
1278
- }
1279
- }
1294
+ const result = await this._http.PUT("/api/v1/flags/{id}", {
1295
+ params: { path: { id: flag.id } },
1296
+ body
1280
1297
  });
1281
1298
  if (result.error !== void 0)
1282
- await checkError2(result.response, `Failed to update context type ${ctId}`);
1283
- data = result.data;
1284
- } catch (err) {
1285
- wrapFetchError2(err);
1286
- }
1287
- if (!data || !data.data) throw new SmplValidationError(`Failed to update context type ${ctId}`);
1288
- return this._parseContextType(data.data);
1289
- }
1290
- /** List all context types. */
1291
- async listContextTypes() {
1292
- let data;
1293
- try {
1294
- const result = await this._appHttp.GET("/api/v1/context_types");
1295
- if (result.error !== void 0)
1296
- await checkError2(result.response, "Failed to list context types");
1297
- data = result.data;
1298
- } catch (err) {
1299
- wrapFetchError2(err);
1300
- }
1301
- if (!data || !data.data) throw new SmplValidationError("Failed to list context types");
1302
- return data.data.map((item) => this._parseContextType(item));
1303
- }
1304
- /** Delete a context type. */
1305
- async deleteContextType(ctId) {
1306
- try {
1307
- const result = await this._appHttp.DELETE("/api/v1/context_types/{id}", {
1308
- params: { path: { id: ctId } }
1309
- });
1310
- if (result.error !== void 0 && result.response.status !== 204)
1311
- await checkError2(result.response, `Failed to delete context type ${ctId}`);
1312
- } catch (err) {
1313
- wrapFetchError2(err);
1314
- }
1315
- }
1316
- /** List context instances filtered by context type key. */
1317
- async listContexts(options) {
1318
- let data;
1319
- try {
1320
- const result = await this._appHttp.GET("/api/v1/contexts", {
1321
- params: { query: { "filter[context_type_id]": options.contextTypeKey } }
1322
- });
1323
- if (result.error !== void 0) await checkError2(result.response, "Failed to list contexts");
1299
+ await checkError2(result.response, `Failed to update flag ${flag.id}`);
1324
1300
  data = result.data;
1325
1301
  } catch (err) {
1326
1302
  wrapFetchError2(err);
1327
1303
  }
1328
- return data?.data ?? [];
1304
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1305
+ return this._resourceToModel(data.data);
1329
1306
  }
1330
1307
  // ------------------------------------------------------------------
1331
1308
  // Runtime: typed flag handles
1332
1309
  // ------------------------------------------------------------------
1333
- /** Declare a boolean flag handle. */
1334
- boolFlag(key, defaultValue) {
1335
- const handle = new BoolFlagHandle(this, key, defaultValue);
1310
+ /** Declare a boolean flag handle for runtime evaluation. */
1311
+ booleanFlag(key, defaultValue) {
1312
+ const handle = new BooleanFlag(this, {
1313
+ id: null,
1314
+ key,
1315
+ name: key,
1316
+ type: "BOOLEAN",
1317
+ default: defaultValue,
1318
+ values: [],
1319
+ description: null,
1320
+ environments: {},
1321
+ createdAt: null,
1322
+ updatedAt: null
1323
+ });
1336
1324
  this._handles[key] = handle;
1337
1325
  return handle;
1338
1326
  }
1339
- /** Declare a string flag handle. */
1327
+ /** Declare a string flag handle for runtime evaluation. */
1340
1328
  stringFlag(key, defaultValue) {
1341
- const handle = new StringFlagHandle(this, key, defaultValue);
1329
+ const handle = new StringFlag(this, {
1330
+ id: null,
1331
+ key,
1332
+ name: key,
1333
+ type: "STRING",
1334
+ default: defaultValue,
1335
+ values: [],
1336
+ description: null,
1337
+ environments: {},
1338
+ createdAt: null,
1339
+ updatedAt: null
1340
+ });
1342
1341
  this._handles[key] = handle;
1343
1342
  return handle;
1344
1343
  }
1345
- /** Declare a numeric flag handle. */
1344
+ /** Declare a numeric flag handle for runtime evaluation. */
1346
1345
  numberFlag(key, defaultValue) {
1347
- const handle = new NumberFlagHandle(this, key, defaultValue);
1346
+ const handle = new NumberFlag(this, {
1347
+ id: null,
1348
+ key,
1349
+ name: key,
1350
+ type: "NUMERIC",
1351
+ default: defaultValue,
1352
+ values: [],
1353
+ description: null,
1354
+ environments: {},
1355
+ createdAt: null,
1356
+ updatedAt: null
1357
+ });
1348
1358
  this._handles[key] = handle;
1349
1359
  return handle;
1350
1360
  }
1351
- /** Declare a JSON flag handle. */
1361
+ /** Declare a JSON flag handle for runtime evaluation. */
1352
1362
  jsonFlag(key, defaultValue) {
1353
- const handle = new JsonFlagHandle(this, key, defaultValue);
1363
+ const handle = new JsonFlag(this, {
1364
+ id: null,
1365
+ key,
1366
+ name: key,
1367
+ type: "JSON",
1368
+ default: defaultValue,
1369
+ values: [],
1370
+ description: null,
1371
+ environments: {},
1372
+ createdAt: null,
1373
+ updatedAt: null
1374
+ });
1354
1375
  this._handles[key] = handle;
1355
1376
  return handle;
1356
1377
  }
@@ -1360,41 +1381,32 @@ var FlagsClient = class {
1360
1381
  /**
1361
1382
  * Register a context provider function.
1362
1383
  *
1363
- * Called on every `handle.get()` to supply the current evaluation
1364
- * context. Can also be used as a decorator:
1365
- *
1366
- * ```typescript
1367
- * client.flags.setContextProvider(() => [
1368
- * new Context("user", userId, { plan: userPlan }),
1369
- * ]);
1370
- * ```
1384
+ * Called on every `handle.get()` to supply the current evaluation context.
1371
1385
  */
1372
1386
  setContextProvider(fn) {
1373
1387
  this._contextProvider = fn;
1374
1388
  }
1375
1389
  /**
1376
1390
  * Register a context provider — decorator-style alias.
1377
- *
1378
- * ```typescript
1379
- * const provider = client.flags.contextProvider(() => [...]);
1380
- * ```
1381
1391
  */
1382
1392
  contextProvider(fn) {
1383
1393
  this._contextProvider = fn;
1384
1394
  return fn;
1385
1395
  }
1386
1396
  // ------------------------------------------------------------------
1387
- // Runtime: connect / disconnect / refresh
1397
+ // Runtime: initialize / disconnect / refresh
1388
1398
  // ------------------------------------------------------------------
1389
1399
  /**
1390
- * Connect to an environment: fetch flag definitions, register on
1391
- * shared WebSocket, enable local evaluation.
1392
- * @internalcalled by SmplClient.connect().
1400
+ * Initialize the flags runtime: fetch definitions and wire WebSocket.
1401
+ *
1402
+ * Idempotentsafe to call multiple times. Must be called (and awaited)
1403
+ * before using `.get()` on flag handles.
1393
1404
  */
1394
- async _connectInternal(environment) {
1395
- this._environment = environment;
1405
+ async initialize() {
1406
+ if (this._initialized) return;
1407
+ this._environment = this._parent?._environment ?? null;
1396
1408
  await this._fetchAllFlags();
1397
- this._connected = true;
1409
+ this._initialized = true;
1398
1410
  this._cache.clear();
1399
1411
  this._wsManager = this._ensureWs();
1400
1412
  this._wsManager.on("flag_changed", this._handleFlagChanged);
@@ -1410,7 +1422,7 @@ var FlagsClient = class {
1410
1422
  await this._flushContexts();
1411
1423
  this._flagStore = {};
1412
1424
  this._cache.clear();
1413
- this._connected = false;
1425
+ this._initialized = false;
1414
1426
  this._environment = null;
1415
1427
  }
1416
1428
  /** Re-fetch all flag definitions and clear cache. */
@@ -1431,22 +1443,27 @@ var FlagsClient = class {
1431
1443
  return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
1432
1444
  }
1433
1445
  // ------------------------------------------------------------------
1434
- // Runtime: change listeners
1446
+ // Runtime: change listeners (dual-mode)
1435
1447
  // ------------------------------------------------------------------
1436
- /** Register a global change listener that fires for any flag change. */
1437
- onChangeAny(callback) {
1438
- this._globalListeners.push(callback);
1439
- return callback;
1440
- }
1441
1448
  /**
1442
- * Register a global change listener — decorator-style alias.
1449
+ * Register a change listener.
1443
1450
  *
1444
- * ```typescript
1445
- * const listener = client.flags.onChange((event) => { ... });
1446
- * ```
1451
+ * - `onChange(callback)` — fires for any flag change (global).
1452
+ * - `onChange(key, callback)` fires only for the specified flag key.
1447
1453
  */
1448
- onChange(callback) {
1449
- return this.onChangeAny(callback);
1454
+ onChange(callbackOrKey, callback) {
1455
+ if (typeof callbackOrKey === "function") {
1456
+ this._globalListeners.push(callbackOrKey);
1457
+ } else {
1458
+ const key = callbackOrKey;
1459
+ if (!callback) {
1460
+ throw new SmplError("onChange(key, callback) requires a callback function.");
1461
+ }
1462
+ if (!this._keyListeners.has(key)) {
1463
+ this._keyListeners.set(key, []);
1464
+ }
1465
+ this._keyListeners.get(key).push(callback);
1466
+ }
1450
1467
  }
1451
1468
  // ------------------------------------------------------------------
1452
1469
  // Runtime: context registration
@@ -1455,7 +1472,7 @@ var FlagsClient = class {
1455
1472
  * Explicitly register context(s) for background batch registration.
1456
1473
  *
1457
1474
  * Accepts a single Context or an array. Fire-and-forget — never
1458
- * blocks. Works before `connect()` is called.
1475
+ * blocks. Works before `initialize()` is called.
1459
1476
  */
1460
1477
  register(context) {
1461
1478
  if (Array.isArray(context)) {
@@ -1473,8 +1490,6 @@ var FlagsClient = class {
1473
1490
  // ------------------------------------------------------------------
1474
1491
  /**
1475
1492
  * Tier 1 explicit evaluation — stateless, no provider or cache.
1476
- *
1477
- * Useful for scripts, one-off jobs, and infrastructure code.
1478
1493
  */
1479
1494
  async evaluate(key, options) {
1480
1495
  const evalDict = contextsToEvalDict(options.context);
@@ -1482,7 +1497,7 @@ var FlagsClient = class {
1482
1497
  evalDict["service"] = { key: this._parent._service };
1483
1498
  }
1484
1499
  let flagDef = null;
1485
- if (this._connected && key in this._flagStore) {
1500
+ if (this._initialized && key in this._flagStore) {
1486
1501
  flagDef = this._flagStore[key];
1487
1502
  } else {
1488
1503
  const flags = await this._fetchFlagsList();
@@ -1503,8 +1518,8 @@ var FlagsClient = class {
1503
1518
  // ------------------------------------------------------------------
1504
1519
  /** @internal */
1505
1520
  _evaluateHandle(key, defaultValue, context) {
1506
- if (!this._connected) {
1507
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
1521
+ if (!this._initialized) {
1522
+ throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
1508
1523
  }
1509
1524
  let evalDict;
1510
1525
  if (context !== null) {
@@ -1541,6 +1556,19 @@ var FlagsClient = class {
1541
1556
  return value;
1542
1557
  }
1543
1558
  // ------------------------------------------------------------------
1559
+ // Internal: _connectInternal (called by SmplClient for backward compat)
1560
+ // ------------------------------------------------------------------
1561
+ /** @internal — called by SmplClient constructor / lazy init. */
1562
+ async _connectInternal(environment) {
1563
+ this._environment = environment;
1564
+ await this._fetchAllFlags();
1565
+ this._initialized = true;
1566
+ this._cache.clear();
1567
+ this._wsManager = this._ensureWs();
1568
+ this._wsManager.on("flag_changed", this._handleFlagChanged);
1569
+ this._wsManager.on("flag_deleted", this._handleFlagDeleted);
1570
+ }
1571
+ // ------------------------------------------------------------------
1544
1572
  // Internal: event handlers (called by SharedWebSocket)
1545
1573
  // ------------------------------------------------------------------
1546
1574
  _handleFlagChanged = (data) => {
@@ -1592,9 +1620,9 @@ var FlagsClient = class {
1592
1620
  } catch {
1593
1621
  }
1594
1622
  }
1595
- const handle = this._handles[flagKey];
1596
- if (handle) {
1597
- for (const cb of handle._listeners) {
1623
+ const keyCallbacks = this._keyListeners.get(flagKey);
1624
+ if (keyCallbacks) {
1625
+ for (const cb of keyCallbacks) {
1598
1626
  try {
1599
1627
  cb(event);
1600
1628
  } catch {
@@ -1630,10 +1658,11 @@ var FlagsClient = class {
1630
1658
  // ------------------------------------------------------------------
1631
1659
  // Internal: model conversion
1632
1660
  // ------------------------------------------------------------------
1661
+ /** @internal */
1633
1662
  _resourceToModel(resource) {
1634
1663
  const attrs = resource.attributes;
1635
1664
  return new Flag(this, {
1636
- id: resource.id ?? "",
1665
+ id: resource.id ?? null,
1637
1666
  key: attrs.key,
1638
1667
  name: attrs.name,
1639
1668
  type: attrs.type,
@@ -1657,13 +1686,567 @@ var FlagsClient = class {
1657
1686
  environments: attrs.environments ?? {}
1658
1687
  };
1659
1688
  }
1660
- _parseContextType(data) {
1661
- const attrs = data.attributes ?? {};
1662
- return new ContextType({
1663
- id: data.id ?? "",
1689
+ };
1690
+
1691
+ // src/logging/client.ts
1692
+ import createClient3 from "openapi-fetch";
1693
+
1694
+ // src/logging/models.ts
1695
+ var Logger = class {
1696
+ /** UUID of the logger, or `null` if unsaved. */
1697
+ id;
1698
+ /** Unique key (dot-separated hierarchy). */
1699
+ key;
1700
+ /** Human-readable display name. */
1701
+ name;
1702
+ /** Base log level, or null if inherited. */
1703
+ level;
1704
+ /** UUID of the parent log group, or null. */
1705
+ group;
1706
+ /** Whether this logger is managed by the platform. */
1707
+ managed;
1708
+ /** Observed sources (services that report this logger). */
1709
+ sources;
1710
+ /** Per-environment level overrides. */
1711
+ environments;
1712
+ /** When the logger was created. */
1713
+ createdAt;
1714
+ /** When the logger was last updated. */
1715
+ updatedAt;
1716
+ /** @internal */
1717
+ _client;
1718
+ /** @internal */
1719
+ constructor(client, fields) {
1720
+ this._client = client;
1721
+ this.id = fields.id;
1722
+ this.key = fields.key;
1723
+ this.name = fields.name;
1724
+ this.level = fields.level;
1725
+ this.group = fields.group;
1726
+ this.managed = fields.managed;
1727
+ this.sources = fields.sources;
1728
+ this.environments = fields.environments;
1729
+ this.createdAt = fields.createdAt;
1730
+ this.updatedAt = fields.updatedAt;
1731
+ }
1732
+ /**
1733
+ * Persist this logger to the server.
1734
+ *
1735
+ * POST if `id` is null (new), PUT if `id` is set (update).
1736
+ */
1737
+ async save() {
1738
+ const saved = await this._client._saveLogger(this);
1739
+ this._apply(saved);
1740
+ }
1741
+ /** Set the base log level (sync local mutation). */
1742
+ setLevel(level) {
1743
+ this.level = level;
1744
+ }
1745
+ /** Clear the base log level (sync local mutation). */
1746
+ clearLevel() {
1747
+ this.level = null;
1748
+ }
1749
+ /** Set an environment-specific log level (sync local mutation). */
1750
+ setEnvironmentLevel(env, level) {
1751
+ const envs = { ...this.environments };
1752
+ envs[env] = { ...envs[env] ?? {}, level };
1753
+ this.environments = envs;
1754
+ }
1755
+ /** Clear an environment-specific log level (sync local mutation). */
1756
+ clearEnvironmentLevel(env) {
1757
+ const envs = { ...this.environments };
1758
+ if (envs[env]) {
1759
+ const entry = { ...envs[env] };
1760
+ delete entry.level;
1761
+ envs[env] = entry;
1762
+ this.environments = envs;
1763
+ }
1764
+ }
1765
+ /** Clear all environment-specific log levels (sync local mutation). */
1766
+ clearAllEnvironmentLevels() {
1767
+ this.environments = {};
1768
+ }
1769
+ /** @internal — copy all fields from another Logger instance. */
1770
+ _apply(other) {
1771
+ this.id = other.id;
1772
+ this.key = other.key;
1773
+ this.name = other.name;
1774
+ this.level = other.level;
1775
+ this.group = other.group;
1776
+ this.managed = other.managed;
1777
+ this.sources = other.sources;
1778
+ this.environments = other.environments;
1779
+ this.createdAt = other.createdAt;
1780
+ this.updatedAt = other.updatedAt;
1781
+ }
1782
+ toString() {
1783
+ return `Logger(key=${this.key}, level=${this.level})`;
1784
+ }
1785
+ };
1786
+ var LogGroup = class {
1787
+ /** UUID of the log group, or `null` if unsaved. */
1788
+ id;
1789
+ /** Unique key. */
1790
+ key;
1791
+ /** Human-readable display name. */
1792
+ name;
1793
+ /** Base log level, or null if inherited. */
1794
+ level;
1795
+ /** UUID of the parent log group, or null. */
1796
+ group;
1797
+ /** Per-environment level overrides. */
1798
+ environments;
1799
+ /** When the log group was created. */
1800
+ createdAt;
1801
+ /** When the log group was last updated. */
1802
+ updatedAt;
1803
+ /** @internal */
1804
+ _client;
1805
+ /** @internal */
1806
+ constructor(client, fields) {
1807
+ this._client = client;
1808
+ this.id = fields.id;
1809
+ this.key = fields.key;
1810
+ this.name = fields.name;
1811
+ this.level = fields.level;
1812
+ this.group = fields.group;
1813
+ this.environments = fields.environments;
1814
+ this.createdAt = fields.createdAt;
1815
+ this.updatedAt = fields.updatedAt;
1816
+ }
1817
+ /**
1818
+ * Persist this log group to the server.
1819
+ *
1820
+ * POST if `id` is null (new), PUT if `id` is set (update).
1821
+ */
1822
+ async save() {
1823
+ const saved = await this._client._saveLogGroup(this);
1824
+ this._apply(saved);
1825
+ }
1826
+ /** Set the base log level (sync local mutation). */
1827
+ setLevel(level) {
1828
+ this.level = level;
1829
+ }
1830
+ /** Clear the base log level (sync local mutation). */
1831
+ clearLevel() {
1832
+ this.level = null;
1833
+ }
1834
+ /** Set an environment-specific log level (sync local mutation). */
1835
+ setEnvironmentLevel(env, level) {
1836
+ const envs = { ...this.environments };
1837
+ envs[env] = { ...envs[env] ?? {}, level };
1838
+ this.environments = envs;
1839
+ }
1840
+ /** Clear an environment-specific log level (sync local mutation). */
1841
+ clearEnvironmentLevel(env) {
1842
+ const envs = { ...this.environments };
1843
+ if (envs[env]) {
1844
+ const entry = { ...envs[env] };
1845
+ delete entry.level;
1846
+ envs[env] = entry;
1847
+ this.environments = envs;
1848
+ }
1849
+ }
1850
+ /** Clear all environment-specific log levels (sync local mutation). */
1851
+ clearAllEnvironmentLevels() {
1852
+ this.environments = {};
1853
+ }
1854
+ /** @internal — copy all fields from another LogGroup instance. */
1855
+ _apply(other) {
1856
+ this.id = other.id;
1857
+ this.key = other.key;
1858
+ this.name = other.name;
1859
+ this.level = other.level;
1860
+ this.group = other.group;
1861
+ this.environments = other.environments;
1862
+ this.createdAt = other.createdAt;
1863
+ this.updatedAt = other.updatedAt;
1864
+ }
1865
+ toString() {
1866
+ return `LogGroup(key=${this.key}, level=${this.level})`;
1867
+ }
1868
+ };
1869
+
1870
+ // src/logging/client.ts
1871
+ var LOGGING_BASE_URL = "https://logging.smplkit.com";
1872
+ async function checkError3(response, _context) {
1873
+ const body = await response.text().catch(() => "");
1874
+ throwForStatus(response.status, body);
1875
+ }
1876
+ function wrapFetchError3(err) {
1877
+ if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
1878
+ throw err;
1879
+ }
1880
+ if (err instanceof TypeError) {
1881
+ throw new SmplConnectionError(`Network error: ${err.message}`);
1882
+ }
1883
+ throw new SmplConnectionError(
1884
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1885
+ );
1886
+ }
1887
+ var LoggingClient = class {
1888
+ /** @internal */
1889
+ _apiKey;
1890
+ /** @internal */
1891
+ _baseUrl = LOGGING_BASE_URL;
1892
+ /** @internal */
1893
+ _http;
1894
+ /** @internal — set by SmplClient after construction. */
1895
+ _parent = null;
1896
+ _ensureWs;
1897
+ _wsManager = null;
1898
+ _started = false;
1899
+ _globalListeners = [];
1900
+ _keyListeners = /* @__PURE__ */ new Map();
1901
+ /** @internal */
1902
+ constructor(apiKey, ensureWs, timeout) {
1903
+ this._apiKey = apiKey;
1904
+ this._ensureWs = ensureWs;
1905
+ const ms = timeout ?? 3e4;
1906
+ this._http = createClient3({
1907
+ baseUrl: LOGGING_BASE_URL,
1908
+ headers: {
1909
+ Authorization: `Bearer ${apiKey}`,
1910
+ Accept: "application/json"
1911
+ },
1912
+ fetch: async (request) => {
1913
+ const controller = new AbortController();
1914
+ const timer = setTimeout(() => controller.abort(), ms);
1915
+ try {
1916
+ return await fetch(new Request(request, { signal: controller.signal }));
1917
+ } catch (err) {
1918
+ if (err instanceof DOMException && err.name === "AbortError") {
1919
+ throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1920
+ }
1921
+ throw err;
1922
+ } finally {
1923
+ clearTimeout(timer);
1924
+ }
1925
+ }
1926
+ });
1927
+ }
1928
+ // ------------------------------------------------------------------
1929
+ // Management: Logger factory
1930
+ // ------------------------------------------------------------------
1931
+ /** Create an unsaved logger. Call `.save()` to persist. */
1932
+ new(key, options) {
1933
+ return new Logger(this, {
1934
+ id: null,
1935
+ key,
1936
+ name: options?.name ?? keyToDisplayName(key),
1937
+ level: null,
1938
+ group: null,
1939
+ managed: options?.managed ?? false,
1940
+ sources: [],
1941
+ environments: {},
1942
+ createdAt: null,
1943
+ updatedAt: null
1944
+ });
1945
+ }
1946
+ // ------------------------------------------------------------------
1947
+ // Management: Logger CRUD
1948
+ // ------------------------------------------------------------------
1949
+ /** Fetch a logger by key. */
1950
+ async get(key) {
1951
+ let data;
1952
+ try {
1953
+ const result = await this._http.GET("/api/v1/loggers", {
1954
+ params: { query: { "filter[key]": key } }
1955
+ });
1956
+ if (result.error !== void 0)
1957
+ await checkError3(result.response, `Logger with key '${key}' not found`);
1958
+ data = result.data;
1959
+ } catch (err) {
1960
+ wrapFetchError3(err);
1961
+ }
1962
+ if (!data || !data.data || data.data.length === 0) {
1963
+ throw new SmplNotFoundError(`Logger with key '${key}' not found`);
1964
+ }
1965
+ return this._loggerToModel(data.data[0]);
1966
+ }
1967
+ /** List all loggers. */
1968
+ async list() {
1969
+ let data;
1970
+ try {
1971
+ const result = await this._http.GET("/api/v1/loggers", {});
1972
+ if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
1973
+ data = result.data;
1974
+ } catch (err) {
1975
+ wrapFetchError3(err);
1976
+ }
1977
+ if (!data) return [];
1978
+ return data.data.map((r) => this._loggerToModel(r));
1979
+ }
1980
+ /** Delete a logger by key. */
1981
+ async delete(key) {
1982
+ const logger = await this.get(key);
1983
+ try {
1984
+ const result = await this._http.DELETE("/api/v1/loggers/{id}", {
1985
+ params: { path: { id: logger.id } }
1986
+ });
1987
+ if (result.error !== void 0 && result.response.status !== 204)
1988
+ await checkError3(result.response, `Failed to delete logger '${key}'`);
1989
+ } catch (err) {
1990
+ wrapFetchError3(err);
1991
+ }
1992
+ }
1993
+ // ------------------------------------------------------------------
1994
+ // Management: LogGroup factory
1995
+ // ------------------------------------------------------------------
1996
+ /** Create an unsaved log group. Call `.save()` to persist. */
1997
+ newGroup(key, options) {
1998
+ return new LogGroup(this, {
1999
+ id: null,
2000
+ key,
2001
+ name: options?.name ?? keyToDisplayName(key),
2002
+ level: null,
2003
+ group: options?.group ?? null,
2004
+ environments: {},
2005
+ createdAt: null,
2006
+ updatedAt: null
2007
+ });
2008
+ }
2009
+ // ------------------------------------------------------------------
2010
+ // Management: LogGroup CRUD
2011
+ // ------------------------------------------------------------------
2012
+ /** Fetch a log group by key. */
2013
+ async getGroup(key) {
2014
+ const groups = await this.listGroups();
2015
+ const match = groups.find((g) => g.key === key);
2016
+ if (!match) {
2017
+ throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
2018
+ }
2019
+ return match;
2020
+ }
2021
+ /** List all log groups. */
2022
+ async listGroups() {
2023
+ let data;
2024
+ try {
2025
+ const result = await this._http.GET("/api/v1/log_groups", {});
2026
+ if (result.error !== void 0)
2027
+ await checkError3(result.response, "Failed to list log groups");
2028
+ data = result.data;
2029
+ } catch (err) {
2030
+ wrapFetchError3(err);
2031
+ }
2032
+ if (!data) return [];
2033
+ return data.data.map((r) => this._groupToModel(r));
2034
+ }
2035
+ /** Delete a log group by key. */
2036
+ async deleteGroup(key) {
2037
+ const group = await this.getGroup(key);
2038
+ try {
2039
+ const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
2040
+ params: { path: { id: group.id } }
2041
+ });
2042
+ if (result.error !== void 0 && result.response.status !== 204)
2043
+ await checkError3(result.response, `Failed to delete log group '${key}'`);
2044
+ } catch (err) {
2045
+ wrapFetchError3(err);
2046
+ }
2047
+ }
2048
+ // ------------------------------------------------------------------
2049
+ // Management: internal save methods
2050
+ // ------------------------------------------------------------------
2051
+ /** @internal — POST or PUT a logger. */
2052
+ async _saveLogger(logger) {
2053
+ const body = {
2054
+ data: {
2055
+ type: "logger",
2056
+ attributes: {
2057
+ key: logger.key,
2058
+ name: logger.name,
2059
+ level: logger.level,
2060
+ group: logger.group,
2061
+ managed: logger.managed,
2062
+ environments: logger.environments
2063
+ }
2064
+ }
2065
+ };
2066
+ if (logger.id === null) {
2067
+ let data;
2068
+ try {
2069
+ const result = await this._http.POST("/api/v1/loggers", { body });
2070
+ if (result.error !== void 0)
2071
+ await checkError3(result.response, "Failed to create logger");
2072
+ data = result.data;
2073
+ } catch (err) {
2074
+ wrapFetchError3(err);
2075
+ }
2076
+ if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
2077
+ return this._loggerToModel(data.data);
2078
+ } else {
2079
+ let data;
2080
+ try {
2081
+ const result = await this._http.PUT("/api/v1/loggers/{id}", {
2082
+ params: { path: { id: logger.id } },
2083
+ body
2084
+ });
2085
+ if (result.error !== void 0)
2086
+ await checkError3(result.response, `Failed to update logger ${logger.id}`);
2087
+ data = result.data;
2088
+ } catch (err) {
2089
+ wrapFetchError3(err);
2090
+ }
2091
+ if (!data || !data.data)
2092
+ throw new SmplValidationError(`Failed to update logger ${logger.id}`);
2093
+ return this._loggerToModel(data.data);
2094
+ }
2095
+ }
2096
+ /** @internal — POST or PUT a log group. */
2097
+ async _saveLogGroup(group) {
2098
+ const body = {
2099
+ data: {
2100
+ type: "log_group",
2101
+ attributes: {
2102
+ key: group.key,
2103
+ name: group.name,
2104
+ level: group.level,
2105
+ group: group.group,
2106
+ environments: group.environments
2107
+ }
2108
+ }
2109
+ };
2110
+ if (group.id === null) {
2111
+ let data;
2112
+ try {
2113
+ const result = await this._http.POST("/api/v1/log_groups", { body });
2114
+ if (result.error !== void 0)
2115
+ await checkError3(result.response, "Failed to create log group");
2116
+ data = result.data;
2117
+ } catch (err) {
2118
+ wrapFetchError3(err);
2119
+ }
2120
+ if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
2121
+ return this._groupToModel(data.data);
2122
+ } else {
2123
+ let data;
2124
+ try {
2125
+ const result = await this._http.PUT("/api/v1/log_groups/{id}", {
2126
+ params: { path: { id: group.id } },
2127
+ body
2128
+ });
2129
+ if (result.error !== void 0)
2130
+ await checkError3(result.response, `Failed to update log group ${group.id}`);
2131
+ data = result.data;
2132
+ } catch (err) {
2133
+ wrapFetchError3(err);
2134
+ }
2135
+ if (!data || !data.data)
2136
+ throw new SmplValidationError(`Failed to update log group ${group.id}`);
2137
+ return this._groupToModel(data.data);
2138
+ }
2139
+ }
2140
+ // ------------------------------------------------------------------
2141
+ // Runtime: start (scaffolded)
2142
+ // ------------------------------------------------------------------
2143
+ /**
2144
+ * Start the logging runtime.
2145
+ *
2146
+ * Fetches existing loggers/groups and wires WebSocket listeners for
2147
+ * live updates. Idempotent — safe to call multiple times.
2148
+ *
2149
+ * Note: Node.js auto-discovery (equivalent to Python's logging module
2150
+ * monkey-patching) is deferred. Management methods work without start().
2151
+ */
2152
+ async start() {
2153
+ if (this._started) return;
2154
+ this._wsManager = this._ensureWs();
2155
+ this._wsManager.on("logger_changed", this._handleLoggerChanged);
2156
+ this._started = true;
2157
+ }
2158
+ // ------------------------------------------------------------------
2159
+ // Runtime: change listeners (dual-mode)
2160
+ // ------------------------------------------------------------------
2161
+ /**
2162
+ * Register a change listener.
2163
+ *
2164
+ * - `onChange(callback)` — fires for any logger change (global).
2165
+ * - `onChange(key, callback)` — fires only for the specified logger key.
2166
+ */
2167
+ onChange(callbackOrKey, callback) {
2168
+ if (typeof callbackOrKey === "function") {
2169
+ this._globalListeners.push(callbackOrKey);
2170
+ } else {
2171
+ const key = callbackOrKey;
2172
+ if (!callback) {
2173
+ throw new SmplError("onChange(key, callback) requires a callback function.");
2174
+ }
2175
+ if (!this._keyListeners.has(key)) {
2176
+ this._keyListeners.set(key, []);
2177
+ }
2178
+ this._keyListeners.get(key).push(callback);
2179
+ }
2180
+ }
2181
+ // ------------------------------------------------------------------
2182
+ // Internal: close
2183
+ // ------------------------------------------------------------------
2184
+ /** @internal */
2185
+ _close() {
2186
+ if (this._wsManager !== null) {
2187
+ this._wsManager.off("logger_changed", this._handleLoggerChanged);
2188
+ this._wsManager = null;
2189
+ }
2190
+ this._started = false;
2191
+ }
2192
+ // ------------------------------------------------------------------
2193
+ // Internal: WebSocket handler
2194
+ // ------------------------------------------------------------------
2195
+ _handleLoggerChanged = (data) => {
2196
+ const key = data.key;
2197
+ if (key) {
2198
+ const level = data.level ?? null;
2199
+ const event = {
2200
+ key,
2201
+ level,
2202
+ source: "websocket"
2203
+ };
2204
+ for (const cb of this._globalListeners) {
2205
+ try {
2206
+ cb(event);
2207
+ } catch {
2208
+ }
2209
+ }
2210
+ const keyCallbacks = this._keyListeners.get(key);
2211
+ if (keyCallbacks) {
2212
+ for (const cb of keyCallbacks) {
2213
+ try {
2214
+ cb(event);
2215
+ } catch {
2216
+ }
2217
+ }
2218
+ }
2219
+ }
2220
+ };
2221
+ // ------------------------------------------------------------------
2222
+ // Internal: model conversion
2223
+ // ------------------------------------------------------------------
2224
+ _loggerToModel(resource) {
2225
+ const attrs = resource.attributes;
2226
+ return new Logger(this, {
2227
+ id: resource.id ?? null,
1664
2228
  key: attrs.key ?? "",
1665
- name: attrs.name ?? "",
1666
- attributes: attrs.attributes ?? {}
2229
+ name: attrs.name,
2230
+ level: attrs.level ?? null,
2231
+ group: attrs.group ?? null,
2232
+ managed: attrs.managed ?? false,
2233
+ sources: attrs.sources ?? [],
2234
+ environments: attrs.environments ?? {},
2235
+ createdAt: attrs.created_at ?? null,
2236
+ updatedAt: attrs.updated_at ?? null
2237
+ });
2238
+ }
2239
+ _groupToModel(resource) {
2240
+ const attrs = resource.attributes;
2241
+ return new LogGroup(this, {
2242
+ id: resource.id ?? null,
2243
+ key: attrs.key ?? "",
2244
+ name: attrs.name,
2245
+ level: attrs.level ?? null,
2246
+ group: attrs.group ?? null,
2247
+ environments: attrs.environments ?? {},
2248
+ createdAt: attrs.created_at ?? null,
2249
+ updatedAt: attrs.updated_at ?? null
1667
2250
  });
1668
2251
  }
1669
2252
  };
@@ -1879,17 +2462,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
1879
2462
  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";
1880
2463
  var NO_SERVICE_MESSAGE = "No service provided. Set one of:\n 1. Pass service in options\n 2. Set the SMPLKIT_SERVICE environment variable";
1881
2464
  var SmplClient = class {
1882
- /** Client for config management-plane operations. */
2465
+ /** Client for config management and runtime. */
1883
2466
  config;
1884
- /** Client for flags management and runtime operations. */
2467
+ /** Client for flags management and runtime. */
1885
2468
  flags;
2469
+ /** Client for logging management and runtime. */
2470
+ logging;
1886
2471
  _wsManager = null;
1887
2472
  _apiKey;
1888
2473
  /** @internal */
1889
2474
  _environment;
1890
2475
  /** @internal */
1891
2476
  _service;
1892
- _connected = false;
1893
2477
  _timeout;
1894
2478
  _appHttp;
1895
2479
  constructor(options = {}) {
@@ -1907,7 +2491,7 @@ var SmplClient = class {
1907
2491
  this._apiKey = apiKey;
1908
2492
  this._timeout = options.timeout ?? 3e4;
1909
2493
  const ms = this._timeout;
1910
- this._appHttp = createClient3({
2494
+ this._appHttp = createClient4({
1911
2495
  baseUrl: APP_BASE_URL2,
1912
2496
  headers: {
1913
2497
  Authorization: `Bearer ${apiKey}`,
@@ -1930,24 +2514,12 @@ var SmplClient = class {
1930
2514
  });
1931
2515
  this.config = new ConfigClient(apiKey, this._timeout);
1932
2516
  this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
2517
+ this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
1933
2518
  this.config._getSharedWs = () => this._ensureWs();
1934
2519
  this.flags._parent = this;
1935
2520
  this.config._parent = this;
1936
- }
1937
- /**
1938
- * Connect to the smplkit platform.
1939
- *
1940
- * Fetches initial flag and config data, opens the shared WebSocket,
1941
- * and registers the service as a context instance (if provided).
1942
- *
1943
- * This method is idempotent — calling it multiple times is safe.
1944
- */
1945
- async connect() {
1946
- if (this._connected) return;
1947
- await this._registerServiceContext();
1948
- await this.flags._connectInternal(this._environment);
1949
- await this.config._connectInternal(this._environment);
1950
- this._connected = true;
2521
+ this.logging._parent = this;
2522
+ void this._registerServiceContext();
1951
2523
  }
1952
2524
  /** @internal */
1953
2525
  async _registerServiceContext() {
@@ -1976,6 +2548,7 @@ var SmplClient = class {
1976
2548
  }
1977
2549
  /** Close the shared WebSocket and release resources. */
1978
2550
  close() {
2551
+ this.logging._close();
1979
2552
  if (this._wsManager !== null) {
1980
2553
  this._wsManager.stop();
1981
2554
  this._wsManager = null;
@@ -2047,28 +2620,43 @@ var Rule = class {
2047
2620
  return result;
2048
2621
  }
2049
2622
  };
2623
+
2624
+ // src/logging/types.ts
2625
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2626
+ LogLevel2["TRACE"] = "TRACE";
2627
+ LogLevel2["DEBUG"] = "DEBUG";
2628
+ LogLevel2["INFO"] = "INFO";
2629
+ LogLevel2["WARN"] = "WARN";
2630
+ LogLevel2["ERROR"] = "ERROR";
2631
+ LogLevel2["FATAL"] = "FATAL";
2632
+ LogLevel2["SILENT"] = "SILENT";
2633
+ return LogLevel2;
2634
+ })(LogLevel || {});
2050
2635
  export {
2051
- BoolFlagHandle,
2636
+ BooleanFlag,
2052
2637
  Config,
2053
2638
  ConfigClient,
2054
2639
  Context,
2055
- ContextType,
2056
2640
  Flag,
2057
2641
  FlagChangeEvent,
2058
2642
  FlagStats,
2059
2643
  FlagsClient,
2060
- JsonFlagHandle,
2061
- NumberFlagHandle,
2644
+ JsonFlag,
2645
+ LiveConfigProxy,
2646
+ LogGroup,
2647
+ LogLevel,
2648
+ Logger,
2649
+ LoggingClient,
2650
+ NumberFlag,
2062
2651
  Rule,
2063
2652
  SharedWebSocket,
2064
2653
  SmplClient,
2065
2654
  SmplConflictError,
2066
2655
  SmplConnectionError,
2067
2656
  SmplError,
2068
- SmplNotConnectedError,
2069
2657
  SmplNotFoundError,
2070
2658
  SmplTimeoutError,
2071
2659
  SmplValidationError,
2072
- StringFlagHandle
2660
+ StringFlag
2073
2661
  };
2074
2662
  //# sourceMappingURL=index.js.map