@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.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,7 @@ var ConfigClient = class {
412
404
  /** @internal — set by SmplClient after construction. */
413
405
  _parent = null;
414
406
  _configCache = {};
415
- _connected = false;
407
+ _initialized = false;
416
408
  _listeners = [];
417
409
  /** @internal */
418
410
  constructor(apiKey, timeout) {
@@ -424,7 +416,6 @@ var ConfigClient = class {
424
416
  Authorization: `Bearer ${apiKey}`,
425
417
  Accept: "application/json"
426
418
  },
427
- // openapi-fetch custom fetch receives a pre-built Request object
428
419
  fetch: async (request) => {
429
420
  const controller = new AbortController();
430
421
  const timer = setTimeout(() => controller.abort(), ms);
@@ -441,23 +432,31 @@ var ConfigClient = class {
441
432
  }
442
433
  });
443
434
  }
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);
435
+ // ------------------------------------------------------------------
436
+ // Management: factory method
437
+ // ------------------------------------------------------------------
438
+ /** Create an unsaved config. Call `.save()` to persist. */
439
+ new(key, options) {
440
+ return new Config(this, {
441
+ id: null,
442
+ key,
443
+ name: options?.name ?? keyToDisplayName(key),
444
+ description: options?.description ?? null,
445
+ parent: options?.parent ?? null,
446
+ items: {},
447
+ environments: {},
448
+ createdAt: null,
449
+ updatedAt: null
450
+ });
457
451
  }
458
- /**
459
- * List all configs for the account.
460
- */
452
+ // ------------------------------------------------------------------
453
+ // Management: CRUD
454
+ // ------------------------------------------------------------------
455
+ /** Fetch a config by key. */
456
+ async get(key) {
457
+ return this._getByKey(key);
458
+ }
459
+ /** List all configs. */
461
460
  async list() {
462
461
  let data;
463
462
  try {
@@ -470,18 +469,31 @@ var ConfigClient = class {
470
469
  if (!data) return [];
471
470
  return data.data.map((r) => resourceToConfig(r, this));
472
471
  }
473
- /**
474
- * Create a new config.
475
- *
476
- * @throws {SmplValidationError} If the server rejects the request.
477
- */
478
- async create(options) {
472
+ /** Delete a config by key. */
473
+ async delete(key) {
474
+ const config = await this.get(key);
475
+ try {
476
+ const result = await this._http.DELETE("/api/v1/configs/{id}", {
477
+ params: { path: { id: config.id } }
478
+ });
479
+ if (result.error !== void 0 && result.response.status !== 204)
480
+ await checkError(result.response, `Failed to delete config '${key}'`);
481
+ } catch (err) {
482
+ wrapFetchError(err);
483
+ }
484
+ }
485
+ // ------------------------------------------------------------------
486
+ // Management: internal save methods (called by Config.save())
487
+ // ------------------------------------------------------------------
488
+ /** @internal — POST a new config. */
489
+ async _createConfig(config) {
479
490
  const body = buildRequestBody({
480
- name: options.name,
481
- key: options.key,
482
- description: options.description,
483
- parent: options.parent,
484
- items: options.items
491
+ name: config.name,
492
+ key: config.key,
493
+ description: config.description,
494
+ parent: config.parent,
495
+ items: config.items,
496
+ environments: config.environments
485
497
  });
486
498
  let data;
487
499
  try {
@@ -494,98 +506,124 @@ var ConfigClient = class {
494
506
  if (!data || !data.data) throw new SmplValidationError("Failed to create config");
495
507
  return resourceToConfig(data.data, this);
496
508
  }
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) {
509
+ /** @internal — PUT a config update. */
510
+ async _updateConfig(config) {
511
+ const body = buildRequestBody({
512
+ id: config.id,
513
+ name: config.name,
514
+ key: config.key,
515
+ description: config.description,
516
+ parent: config.parent,
517
+ items: config.items,
518
+ environments: config.environments
519
+ });
520
+ let data;
504
521
  try {
505
- const result = await this._http.DELETE("/api/v1/configs/{id}", {
506
- params: { path: { id: configId } }
522
+ const result = await this._http.PUT("/api/v1/configs/{id}", {
523
+ params: { path: { id: config.id } },
524
+ body
507
525
  });
508
- if (result.error !== void 0 && result.response.status !== 204)
509
- await checkError(result.response, `Failed to delete config ${configId}`);
526
+ if (result.error !== void 0)
527
+ await checkError(result.response, `Failed to update config ${config.id}`);
528
+ data = result.data;
510
529
  } catch (err) {
511
530
  wrapFetchError(err);
512
531
  }
532
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update config ${config.id}`);
533
+ return resourceToConfig(data.data, this);
513
534
  }
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);
535
+ /** @internal — fetch a config by UUID. */
536
+ async _getById(configId) {
537
+ let data;
538
+ try {
539
+ const result = await this._http.GET("/api/v1/configs/{id}", {
540
+ params: { path: { id: configId } }
541
+ });
542
+ if (result.error !== void 0)
543
+ await checkError(result.response, `Config ${configId} not found`);
544
+ data = result.data;
545
+ } catch (err) {
546
+ wrapFetchError(err);
524
547
  }
525
- this._configCache = cache;
526
- this._connected = true;
548
+ if (!data || !data.data) throw new SmplNotFoundError(`Config ${configId} not found`);
549
+ return resourceToConfig(data.data, this);
527
550
  }
551
+ // ------------------------------------------------------------------
552
+ // Runtime: resolve and subscribe
553
+ // ------------------------------------------------------------------
528
554
  /**
529
- * Read a resolved config value (prescriptive access).
555
+ * Resolve a config's values for the current environment.
530
556
  *
531
- * Requires {@link SmplClient.connect} to have been called.
557
+ * Returns a flat dict of resolved key-value pairs, walking the
558
+ * parent chain and applying environment overrides.
532
559
  *
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.
560
+ * Optionally pass a model class to map the resolved values.
538
561
  */
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;
562
+ async resolve(key, model) {
563
+ await this._ensureInitialized();
564
+ const values = this._configCache[key];
565
+ if (values === void 0) {
566
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
546
567
  }
547
- if (itemKey === void 0) {
548
- return { ...resolved };
568
+ if (model) {
569
+ return new model(values);
549
570
  }
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;
571
+ return values;
560
572
  }
561
573
  /**
562
- * Return a config value as a number, or `defaultValue` if absent or not a number.
574
+ * Subscribe to a config's values returns a live proxy that
575
+ * auto-updates when the underlying config changes.
563
576
  *
564
- * @throws {SmplNotConnectedError} If connect() has not been called.
577
+ * Optionally pass a model class to map the resolved values.
565
578
  */
566
- getInt(configKey, itemKey, defaultValue = null) {
567
- const value = this.getValue(configKey, itemKey);
568
- return typeof value === "number" ? value : defaultValue;
579
+ async subscribe(key, model) {
580
+ await this._ensureInitialized();
581
+ if (!(key in this._configCache)) {
582
+ throw new SmplNotFoundError(`Config with key '${key}' not found in cache`);
583
+ }
584
+ return new LiveConfigProxy(this, key, model);
569
585
  }
586
+ // ------------------------------------------------------------------
587
+ // Runtime: change listeners (3-level overloads)
588
+ // ------------------------------------------------------------------
570
589
  /**
571
- * Return a config value as a boolean, or `defaultValue` if absent or not a boolean.
590
+ * Register a change listener.
572
591
  *
573
- * @throws {SmplNotConnectedError} If connect() has not been called.
592
+ * - `onChange(callback)` fires for any config change (global).
593
+ * - `onChange(configKey, callback)` — fires for changes to a specific config.
594
+ * - `onChange(configKey, itemKey, callback)` — fires for a specific item.
574
595
  */
575
- getBool(configKey, itemKey, defaultValue = null) {
576
- const value = this.getValue(configKey, itemKey);
577
- return typeof value === "boolean" ? value : defaultValue;
596
+ onChange(callbackOrConfigKey, callbackOrItemKey, callback) {
597
+ if (typeof callbackOrConfigKey === "function") {
598
+ this._listeners.push({
599
+ callback: callbackOrConfigKey,
600
+ configKey: null,
601
+ itemKey: null
602
+ });
603
+ } else if (typeof callbackOrItemKey === "function") {
604
+ this._listeners.push({
605
+ callback: callbackOrItemKey,
606
+ configKey: callbackOrConfigKey,
607
+ itemKey: null
608
+ });
609
+ } else if (typeof callbackOrItemKey === "string" && callback) {
610
+ this._listeners.push({
611
+ callback,
612
+ configKey: callbackOrConfigKey,
613
+ itemKey: callbackOrItemKey
614
+ });
615
+ }
578
616
  }
617
+ // ------------------------------------------------------------------
618
+ // Runtime: refresh
619
+ // ------------------------------------------------------------------
579
620
  /**
580
621
  * 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.
622
+ * Fires change listeners for any values that differ.
585
623
  */
586
624
  async refresh() {
587
- if (!this._connected) {
588
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
625
+ if (!this._initialized) {
626
+ throw new SmplError("Config not initialized. Call resolve() or subscribe() first.");
589
627
  }
590
628
  const environment = this._parent?._environment;
591
629
  if (!environment) {
@@ -594,27 +632,62 @@ var ConfigClient = class {
594
632
  const configs = await this.list();
595
633
  const newCache = {};
596
634
  for (const cfg of configs) {
597
- const chain = await cfg._buildChain(this._http);
635
+ const chain = await cfg._buildChain();
598
636
  newCache[cfg.key] = resolveChain(chain, environment);
599
637
  }
600
638
  const oldCache = this._configCache;
601
639
  this._configCache = newCache;
602
640
  this._diffAndFire(oldCache, newCache, "manual");
603
641
  }
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
- });
642
+ // ------------------------------------------------------------------
643
+ // Runtime: lazy initialization
644
+ // ------------------------------------------------------------------
645
+ /** @internal */
646
+ async _ensureInitialized() {
647
+ if (this._initialized) return;
648
+ const environment = this._parent?._environment;
649
+ if (!environment) {
650
+ throw new SmplError("No environment set. Ensure SmplClient is configured.");
651
+ }
652
+ const configs = await this.list();
653
+ const cache = {};
654
+ for (const cfg of configs) {
655
+ const chain = await cfg._buildChain();
656
+ cache[cfg.key] = resolveChain(chain, environment);
657
+ }
658
+ this._configCache = cache;
659
+ this._initialized = true;
660
+ if (this._getSharedWs) {
661
+ const ws = this._getSharedWs();
662
+ ws.on("config_changed", this._handleConfigChanged);
663
+ }
664
+ }
665
+ /** @internal — called by SmplClient for backward compat. */
666
+ async _connectInternal(environment) {
667
+ if (this._initialized) return;
668
+ const configs = await this.list();
669
+ const cache = {};
670
+ for (const cfg of configs) {
671
+ const chain = await cfg._buildChain();
672
+ cache[cfg.key] = resolveChain(chain, environment);
673
+ }
674
+ this._configCache = cache;
675
+ this._initialized = true;
617
676
  }
677
+ /** @internal — get resolved config from cache. Used by LiveConfigProxy. */
678
+ _getCachedConfig(key) {
679
+ return this._configCache[key];
680
+ }
681
+ // ------------------------------------------------------------------
682
+ // Internal: WebSocket handler
683
+ // ------------------------------------------------------------------
684
+ _handleConfigChanged = (_data) => {
685
+ void this.refresh().catch(() => {
686
+ });
687
+ };
688
+ // ------------------------------------------------------------------
689
+ // Internal: change detection
690
+ // ------------------------------------------------------------------
618
691
  /** @internal */
619
692
  _diffAndFire(oldCache, newCache, source) {
620
693
  const allConfigKeys = /* @__PURE__ */ new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);
@@ -645,54 +718,9 @@ var ConfigClient = class {
645
718
  }
646
719
  }
647
720
  }
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
- }
721
+ // ------------------------------------------------------------------
722
+ // Internal: fetch by key
723
+ // ------------------------------------------------------------------
696
724
  async _getByKey(key) {
697
725
  let data;
698
726
  try {
@@ -717,7 +745,7 @@ import createClient2 from "openapi-fetch";
717
745
 
718
746
  // src/flags/models.ts
719
747
  var Flag = class {
720
- /** UUID of the flag. */
748
+ /** UUID of the flag, or `null` if unsaved. */
721
749
  id;
722
750
  /** Unique key within the account. */
723
751
  key;
@@ -754,37 +782,35 @@ var Flag = class {
754
782
  this.updatedAt = fields.updatedAt;
755
783
  }
756
784
  /**
757
- * Update this flag's attributes on the server.
785
+ * Persist this flag to the server.
758
786
  *
759
- * Only provided fields are changed; others retain their current values.
787
+ * POST if `id` is null (new flag), PUT if `id` is set (update).
788
+ * Updates this instance in-place with the server response.
760
789
  */
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);
790
+ async save() {
791
+ if (this.id === null) {
792
+ const created = await this._client._createFlag(this);
793
+ this._apply(created);
794
+ } else {
795
+ const updated = await this._client._updateFlag(this);
796
+ this._apply(updated);
797
+ }
771
798
  }
772
799
  /**
773
- * Add a rule to a specific environment.
800
+ * Add a rule to a specific environment (sync local mutation).
774
801
  *
775
802
  * 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.
803
+ * `Rule(...).environment("env_key")`). No HTTP call is made.
804
+ *
805
+ * @returns `this` for chaining.
778
806
  */
779
- async addRule(builtRule) {
807
+ addRule(builtRule) {
780
808
  const envKey = builtRule.environment;
781
809
  if (!envKey) {
782
810
  throw new Error(
783
811
  `Built rule must include 'environment' key. Use new Rule(...).environment("env_key").when(...).serve(...).build()`
784
812
  );
785
813
  }
786
- const current = await this._client.get(this.id);
787
- this._apply(current);
788
814
  const envs = { ...this.environments };
789
815
  const envData = { ...envs[envKey] ?? { enabled: true, rules: [] } };
790
816
  const rules = [...envData.rules ?? []];
@@ -792,13 +818,43 @@ var Flag = class {
792
818
  rules.push(ruleCopy);
793
819
  envData.rules = rules;
794
820
  envs[envKey] = envData;
795
- const updated = await this._client._updateFlag({
796
- flag: this,
797
- environments: envs
798
- });
799
- this._apply(updated);
821
+ this.environments = envs;
822
+ return this;
800
823
  }
801
- /** @internal */
824
+ /** Enable or disable a flag in a specific environment (sync local mutation). */
825
+ setEnvironmentEnabled(envKey, enabled) {
826
+ const envs = { ...this.environments };
827
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
828
+ envData.enabled = enabled;
829
+ envs[envKey] = envData;
830
+ this.environments = envs;
831
+ }
832
+ /** Set the default value for a specific environment (sync local mutation). */
833
+ setEnvironmentDefault(envKey, defaultValue) {
834
+ const envs = { ...this.environments };
835
+ const envData = { ...envs[envKey] ?? { enabled: false, rules: [] } };
836
+ envData.default = defaultValue;
837
+ envs[envKey] = envData;
838
+ this.environments = envs;
839
+ }
840
+ /** Clear all rules for a specific environment (sync local mutation). */
841
+ clearRules(envKey) {
842
+ const envs = { ...this.environments };
843
+ const envData = envs[envKey];
844
+ if (envData) {
845
+ envs[envKey] = { ...envData, rules: [] };
846
+ this.environments = envs;
847
+ }
848
+ }
849
+ /**
850
+ * Evaluate the flag locally (sync, no HTTP).
851
+ *
852
+ * Requires `initialize()` to have been called.
853
+ */
854
+ get(options) {
855
+ return this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
856
+ }
857
+ /** @internal — copy all fields from another Flag instance. */
802
858
  _apply(other) {
803
859
  this.id = other.id;
804
860
  this.key = other.key;
@@ -815,36 +871,53 @@ var Flag = class {
815
871
  return `Flag(key=${this.key}, type=${this.type}, default=${this.default})`;
816
872
  }
817
873
  };
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;
874
+ var BooleanFlag = class extends Flag {
875
+ get(options) {
876
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
877
+ if (typeof value === "boolean") {
878
+ return value;
879
+ }
880
+ return this.default;
832
881
  }
833
- toString() {
834
- return `ContextType(key=${this.key}, name=${this.name})`;
882
+ };
883
+ var StringFlag = class extends Flag {
884
+ get(options) {
885
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
886
+ if (typeof value === "string") {
887
+ return value;
888
+ }
889
+ return this.default;
835
890
  }
836
891
  };
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(() => "");
847
- throwForStatus(response.status, body);
892
+ var NumberFlag = class extends Flag {
893
+ get(options) {
894
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
895
+ if (typeof value === "number") {
896
+ return value;
897
+ }
898
+ return this.default;
899
+ }
900
+ };
901
+ var JsonFlag = class extends Flag {
902
+ get(options) {
903
+ const value = this._client._evaluateHandle(this.key, this.default, options?.context ?? null);
904
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
905
+ return value;
906
+ }
907
+ return this.default;
908
+ }
909
+ };
910
+
911
+ // src/flags/client.ts
912
+ import jsonLogic from "json-logic-js";
913
+ var FLAGS_BASE_URL = "https://flags.smplkit.com";
914
+ var APP_BASE_URL = "https://app.smplkit.com";
915
+ var CACHE_MAX_SIZE = 1e4;
916
+ var CONTEXT_REGISTRATION_LRU_SIZE = 1e4;
917
+ var CONTEXT_BATCH_FLUSH_SIZE = 100;
918
+ async function checkError2(response, _context) {
919
+ const body = await response.text().catch(() => "");
920
+ throwForStatus(response.status, body);
848
921
  }
849
922
  function wrapFetchError2(err) {
850
923
  if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
@@ -960,88 +1033,6 @@ var FlagStats = class {
960
1033
  this.cacheMisses = cacheMisses;
961
1034
  }
962
1035
  };
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
1036
  var ContextRegistrationBuffer = class {
1046
1037
  _seen = /* @__PURE__ */ new Map();
1047
1038
  _pending = [];
@@ -1085,13 +1076,14 @@ var FlagsClient = class {
1085
1076
  // Runtime state
1086
1077
  _environment = null;
1087
1078
  _flagStore = {};
1088
- _connected = false;
1079
+ _initialized = false;
1089
1080
  _cache = new ResolutionCache();
1090
1081
  _contextProvider = null;
1091
1082
  _contextBuffer = new ContextRegistrationBuffer();
1092
1083
  _handles = {};
1093
1084
  _globalListeners = [];
1094
- // Shared WebSocket (set during connect)
1085
+ _keyListeners = /* @__PURE__ */ new Map();
1086
+ // Shared WebSocket (set during initialize)
1095
1087
  _wsManager = null;
1096
1088
  _ensureWs;
1097
1089
  /** @internal — set by SmplClient after construction. */
@@ -1133,55 +1125,91 @@ var FlagsClient = class {
1133
1125
  });
1134
1126
  }
1135
1127
  // ------------------------------------------------------------------
1136
- // Management methods
1128
+ // Management: factory methods (return unsaved flags)
1137
1129
  // ------------------------------------------------------------------
1138
- /** Create a flag. */
1139
- async create(key, options) {
1140
- let values = options.values;
1141
- if (values === void 0 && options.type === "BOOLEAN") {
1142
- values = [
1130
+ /** Create an unsaved boolean flag. Call `.save()` to persist. */
1131
+ newBooleanFlag(key, options) {
1132
+ return new BooleanFlag(this, {
1133
+ id: null,
1134
+ key,
1135
+ name: options.name ?? keyToDisplayName(key),
1136
+ type: "BOOLEAN",
1137
+ default: options.default,
1138
+ values: [
1143
1139
  { name: "True", value: true },
1144
1140
  { 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);
1141
+ ],
1142
+ description: options.description ?? null,
1143
+ environments: {},
1144
+ createdAt: null,
1145
+ updatedAt: null
1146
+ });
1147
+ }
1148
+ /** Create an unsaved string flag. Call `.save()` to persist. */
1149
+ newStringFlag(key, options) {
1150
+ return new StringFlag(this, {
1151
+ id: null,
1152
+ key,
1153
+ name: options.name ?? keyToDisplayName(key),
1154
+ type: "STRING",
1155
+ default: options.default,
1156
+ values: options.values ?? [],
1157
+ description: options.description ?? null,
1158
+ environments: {},
1159
+ createdAt: null,
1160
+ updatedAt: null
1161
+ });
1170
1162
  }
1171
- /** Fetch a flag by UUID. */
1172
- async get(flagId) {
1163
+ /** Create an unsaved number flag. Call `.save()` to persist. */
1164
+ newNumberFlag(key, options) {
1165
+ return new NumberFlag(this, {
1166
+ id: null,
1167
+ key,
1168
+ name: options.name ?? keyToDisplayName(key),
1169
+ type: "NUMERIC",
1170
+ default: options.default,
1171
+ values: options.values ?? [],
1172
+ description: options.description ?? null,
1173
+ environments: {},
1174
+ createdAt: null,
1175
+ updatedAt: null
1176
+ });
1177
+ }
1178
+ /** Create an unsaved JSON flag. Call `.save()` to persist. */
1179
+ newJsonFlag(key, options) {
1180
+ return new JsonFlag(this, {
1181
+ id: null,
1182
+ key,
1183
+ name: options.name ?? keyToDisplayName(key),
1184
+ type: "JSON",
1185
+ default: options.default,
1186
+ values: options.values ?? [],
1187
+ description: options.description ?? null,
1188
+ environments: {},
1189
+ createdAt: null,
1190
+ updatedAt: null
1191
+ });
1192
+ }
1193
+ // ------------------------------------------------------------------
1194
+ // Management: CRUD
1195
+ // ------------------------------------------------------------------
1196
+ /** Fetch a flag by key. */
1197
+ async get(key) {
1173
1198
  let data;
1174
1199
  try {
1175
- const result = await this._http.GET("/api/v1/flags/{id}", {
1176
- params: { path: { id: flagId } }
1200
+ const result = await this._http.GET("/api/v1/flags", {
1201
+ params: { query: { "filter[key]": key } }
1177
1202
  });
1178
- if (result.error !== void 0) await checkError2(result.response, `Flag ${flagId} not found`);
1203
+ if (result.error !== void 0)
1204
+ await checkError2(result.response, `Flag with key '${key}' not found`);
1179
1205
  data = result.data;
1180
1206
  } catch (err) {
1181
1207
  wrapFetchError2(err);
1182
1208
  }
1183
- if (!data || !data.data) throw new SmplNotFoundError(`Flag ${flagId} not found`);
1184
- return this._resourceToModel(data.data);
1209
+ if (!data || !data.data || data.data.length === 0) {
1210
+ throw new SmplNotFoundError(`Flag with key '${key}' not found`);
1211
+ }
1212
+ return this._resourceToModel(data.data[0]);
1185
1213
  }
1186
1214
  /** List all flags. */
1187
1215
  async list() {
@@ -1196,161 +1224,148 @@ var FlagsClient = class {
1196
1224
  if (!data) return [];
1197
1225
  return data.data.map((r) => this._resourceToModel(r));
1198
1226
  }
1199
- /** Delete a flag by UUID. */
1200
- async delete(flagId) {
1227
+ /** Delete a flag by key. */
1228
+ async delete(key) {
1229
+ const flag = await this.get(key);
1201
1230
  try {
1202
1231
  const result = await this._http.DELETE("/api/v1/flags/{id}", {
1203
- params: { path: { id: flagId } }
1232
+ params: { path: { id: flag.id } }
1204
1233
  });
1205
1234
  if (result.error !== void 0 && result.response.status !== 204)
1206
- await checkError2(result.response, `Failed to delete flag ${flagId}`);
1235
+ await checkError2(result.response, `Failed to delete flag '${key}'`);
1207
1236
  } catch (err) {
1208
1237
  wrapFetchError2(err);
1209
1238
  }
1210
1239
  }
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;
1240
+ // ------------------------------------------------------------------
1241
+ // Management: internal save methods (called by Flag.save())
1242
+ // ------------------------------------------------------------------
1243
+ /** @internal — POST a new flag. */
1244
+ async _createFlag(flag) {
1218
1245
  const body = {
1219
1246
  data: {
1220
1247
  type: "flag",
1221
1248
  attributes: {
1222
1249
  key: flag.key,
1223
- name: options.name !== void 0 ? options.name : flag.name,
1250
+ name: flag.name,
1251
+ description: flag.description ?? "",
1224
1252
  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 } : {}
1253
+ default: flag.default,
1254
+ values: flag.values,
1255
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1229
1256
  }
1230
1257
  }
1231
1258
  };
1232
1259
  let data;
1233
1260
  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}`);
1261
+ const result = await this._http.POST("/api/v1/flags", { body });
1262
+ if (result.error !== void 0) await checkError2(result.response, "Failed to create flag");
1240
1263
  data = result.data;
1241
1264
  } catch (err) {
1242
1265
  wrapFetchError2(err);
1243
1266
  }
1244
- if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1267
+ if (!data || !data.data) throw new SmplValidationError("Failed to create flag");
1245
1268
  return this._resourceToModel(data.data);
1246
1269
  }
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 } }
1270
+ /** @internal — PUT a flag update. */
1271
+ async _updateFlag(flag) {
1272
+ const body = {
1273
+ data: {
1274
+ type: "flag",
1275
+ attributes: {
1276
+ key: flag.key,
1277
+ name: flag.name,
1278
+ type: flag.type,
1279
+ default: flag.default,
1280
+ values: flag.values,
1281
+ description: flag.description ?? "",
1282
+ ...Object.keys(flag.environments).length > 0 ? { environments: flag.environments } : {}
1257
1283
  }
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) {
1284
+ }
1285
+ };
1270
1286
  let data;
1271
1287
  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
- }
1288
+ const result = await this._http.PUT("/api/v1/flags/{id}", {
1289
+ params: { path: { id: flag.id } },
1290
+ body
1280
1291
  });
1281
1292
  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");
1293
+ await checkError2(result.response, `Failed to update flag ${flag.id}`);
1324
1294
  data = result.data;
1325
1295
  } catch (err) {
1326
1296
  wrapFetchError2(err);
1327
1297
  }
1328
- return data?.data ?? [];
1298
+ if (!data || !data.data) throw new SmplValidationError(`Failed to update flag ${flag.id}`);
1299
+ return this._resourceToModel(data.data);
1329
1300
  }
1330
1301
  // ------------------------------------------------------------------
1331
1302
  // Runtime: typed flag handles
1332
1303
  // ------------------------------------------------------------------
1333
- /** Declare a boolean flag handle. */
1334
- boolFlag(key, defaultValue) {
1335
- const handle = new BoolFlagHandle(this, key, defaultValue);
1304
+ /** Declare a boolean flag handle for runtime evaluation. */
1305
+ booleanFlag(key, defaultValue) {
1306
+ const handle = new BooleanFlag(this, {
1307
+ id: null,
1308
+ key,
1309
+ name: key,
1310
+ type: "BOOLEAN",
1311
+ default: defaultValue,
1312
+ values: [],
1313
+ description: null,
1314
+ environments: {},
1315
+ createdAt: null,
1316
+ updatedAt: null
1317
+ });
1336
1318
  this._handles[key] = handle;
1337
1319
  return handle;
1338
1320
  }
1339
- /** Declare a string flag handle. */
1321
+ /** Declare a string flag handle for runtime evaluation. */
1340
1322
  stringFlag(key, defaultValue) {
1341
- const handle = new StringFlagHandle(this, key, defaultValue);
1323
+ const handle = new StringFlag(this, {
1324
+ id: null,
1325
+ key,
1326
+ name: key,
1327
+ type: "STRING",
1328
+ default: defaultValue,
1329
+ values: [],
1330
+ description: null,
1331
+ environments: {},
1332
+ createdAt: null,
1333
+ updatedAt: null
1334
+ });
1342
1335
  this._handles[key] = handle;
1343
1336
  return handle;
1344
1337
  }
1345
- /** Declare a numeric flag handle. */
1338
+ /** Declare a numeric flag handle for runtime evaluation. */
1346
1339
  numberFlag(key, defaultValue) {
1347
- const handle = new NumberFlagHandle(this, key, defaultValue);
1340
+ const handle = new NumberFlag(this, {
1341
+ id: null,
1342
+ key,
1343
+ name: key,
1344
+ type: "NUMERIC",
1345
+ default: defaultValue,
1346
+ values: [],
1347
+ description: null,
1348
+ environments: {},
1349
+ createdAt: null,
1350
+ updatedAt: null
1351
+ });
1348
1352
  this._handles[key] = handle;
1349
1353
  return handle;
1350
1354
  }
1351
- /** Declare a JSON flag handle. */
1355
+ /** Declare a JSON flag handle for runtime evaluation. */
1352
1356
  jsonFlag(key, defaultValue) {
1353
- const handle = new JsonFlagHandle(this, key, defaultValue);
1357
+ const handle = new JsonFlag(this, {
1358
+ id: null,
1359
+ key,
1360
+ name: key,
1361
+ type: "JSON",
1362
+ default: defaultValue,
1363
+ values: [],
1364
+ description: null,
1365
+ environments: {},
1366
+ createdAt: null,
1367
+ updatedAt: null
1368
+ });
1354
1369
  this._handles[key] = handle;
1355
1370
  return handle;
1356
1371
  }
@@ -1360,41 +1375,32 @@ var FlagsClient = class {
1360
1375
  /**
1361
1376
  * Register a context provider function.
1362
1377
  *
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
- * ```
1378
+ * Called on every `handle.get()` to supply the current evaluation context.
1371
1379
  */
1372
1380
  setContextProvider(fn) {
1373
1381
  this._contextProvider = fn;
1374
1382
  }
1375
1383
  /**
1376
1384
  * Register a context provider — decorator-style alias.
1377
- *
1378
- * ```typescript
1379
- * const provider = client.flags.contextProvider(() => [...]);
1380
- * ```
1381
1385
  */
1382
1386
  contextProvider(fn) {
1383
1387
  this._contextProvider = fn;
1384
1388
  return fn;
1385
1389
  }
1386
1390
  // ------------------------------------------------------------------
1387
- // Runtime: connect / disconnect / refresh
1391
+ // Runtime: initialize / disconnect / refresh
1388
1392
  // ------------------------------------------------------------------
1389
1393
  /**
1390
- * Connect to an environment: fetch flag definitions, register on
1391
- * shared WebSocket, enable local evaluation.
1392
- * @internalcalled by SmplClient.connect().
1394
+ * Initialize the flags runtime: fetch definitions and wire WebSocket.
1395
+ *
1396
+ * Idempotentsafe to call multiple times. Must be called (and awaited)
1397
+ * before using `.get()` on flag handles.
1393
1398
  */
1394
- async _connectInternal(environment) {
1395
- this._environment = environment;
1399
+ async initialize() {
1400
+ if (this._initialized) return;
1401
+ this._environment = this._parent?._environment ?? null;
1396
1402
  await this._fetchAllFlags();
1397
- this._connected = true;
1403
+ this._initialized = true;
1398
1404
  this._cache.clear();
1399
1405
  this._wsManager = this._ensureWs();
1400
1406
  this._wsManager.on("flag_changed", this._handleFlagChanged);
@@ -1410,7 +1416,7 @@ var FlagsClient = class {
1410
1416
  await this._flushContexts();
1411
1417
  this._flagStore = {};
1412
1418
  this._cache.clear();
1413
- this._connected = false;
1419
+ this._initialized = false;
1414
1420
  this._environment = null;
1415
1421
  }
1416
1422
  /** Re-fetch all flag definitions and clear cache. */
@@ -1431,22 +1437,27 @@ var FlagsClient = class {
1431
1437
  return new FlagStats(this._cache.cacheHits, this._cache.cacheMisses);
1432
1438
  }
1433
1439
  // ------------------------------------------------------------------
1434
- // Runtime: change listeners
1440
+ // Runtime: change listeners (dual-mode)
1435
1441
  // ------------------------------------------------------------------
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
1442
  /**
1442
- * Register a global change listener — decorator-style alias.
1443
+ * Register a change listener.
1443
1444
  *
1444
- * ```typescript
1445
- * const listener = client.flags.onChange((event) => { ... });
1446
- * ```
1445
+ * - `onChange(callback)` — fires for any flag change (global).
1446
+ * - `onChange(key, callback)` fires only for the specified flag key.
1447
1447
  */
1448
- onChange(callback) {
1449
- return this.onChangeAny(callback);
1448
+ onChange(callbackOrKey, callback) {
1449
+ if (typeof callbackOrKey === "function") {
1450
+ this._globalListeners.push(callbackOrKey);
1451
+ } else {
1452
+ const key = callbackOrKey;
1453
+ if (!callback) {
1454
+ throw new SmplError("onChange(key, callback) requires a callback function.");
1455
+ }
1456
+ if (!this._keyListeners.has(key)) {
1457
+ this._keyListeners.set(key, []);
1458
+ }
1459
+ this._keyListeners.get(key).push(callback);
1460
+ }
1450
1461
  }
1451
1462
  // ------------------------------------------------------------------
1452
1463
  // Runtime: context registration
@@ -1455,7 +1466,7 @@ var FlagsClient = class {
1455
1466
  * Explicitly register context(s) for background batch registration.
1456
1467
  *
1457
1468
  * Accepts a single Context or an array. Fire-and-forget — never
1458
- * blocks. Works before `connect()` is called.
1469
+ * blocks. Works before `initialize()` is called.
1459
1470
  */
1460
1471
  register(context) {
1461
1472
  if (Array.isArray(context)) {
@@ -1473,8 +1484,6 @@ var FlagsClient = class {
1473
1484
  // ------------------------------------------------------------------
1474
1485
  /**
1475
1486
  * Tier 1 explicit evaluation — stateless, no provider or cache.
1476
- *
1477
- * Useful for scripts, one-off jobs, and infrastructure code.
1478
1487
  */
1479
1488
  async evaluate(key, options) {
1480
1489
  const evalDict = contextsToEvalDict(options.context);
@@ -1482,7 +1491,7 @@ var FlagsClient = class {
1482
1491
  evalDict["service"] = { key: this._parent._service };
1483
1492
  }
1484
1493
  let flagDef = null;
1485
- if (this._connected && key in this._flagStore) {
1494
+ if (this._initialized && key in this._flagStore) {
1486
1495
  flagDef = this._flagStore[key];
1487
1496
  } else {
1488
1497
  const flags = await this._fetchFlagsList();
@@ -1503,8 +1512,8 @@ var FlagsClient = class {
1503
1512
  // ------------------------------------------------------------------
1504
1513
  /** @internal */
1505
1514
  _evaluateHandle(key, defaultValue, context) {
1506
- if (!this._connected) {
1507
- throw new SmplNotConnectedError("SmplClient is not connected. Call client.connect() first.");
1515
+ if (!this._initialized) {
1516
+ throw new SmplError("Flags not initialized. Call await client.flags.initialize() first.");
1508
1517
  }
1509
1518
  let evalDict;
1510
1519
  if (context !== null) {
@@ -1541,6 +1550,19 @@ var FlagsClient = class {
1541
1550
  return value;
1542
1551
  }
1543
1552
  // ------------------------------------------------------------------
1553
+ // Internal: _connectInternal (called by SmplClient for backward compat)
1554
+ // ------------------------------------------------------------------
1555
+ /** @internal — called by SmplClient constructor / lazy init. */
1556
+ async _connectInternal(environment) {
1557
+ this._environment = environment;
1558
+ await this._fetchAllFlags();
1559
+ this._initialized = true;
1560
+ this._cache.clear();
1561
+ this._wsManager = this._ensureWs();
1562
+ this._wsManager.on("flag_changed", this._handleFlagChanged);
1563
+ this._wsManager.on("flag_deleted", this._handleFlagDeleted);
1564
+ }
1565
+ // ------------------------------------------------------------------
1544
1566
  // Internal: event handlers (called by SharedWebSocket)
1545
1567
  // ------------------------------------------------------------------
1546
1568
  _handleFlagChanged = (data) => {
@@ -1592,9 +1614,9 @@ var FlagsClient = class {
1592
1614
  } catch {
1593
1615
  }
1594
1616
  }
1595
- const handle = this._handles[flagKey];
1596
- if (handle) {
1597
- for (const cb of handle._listeners) {
1617
+ const keyCallbacks = this._keyListeners.get(flagKey);
1618
+ if (keyCallbacks) {
1619
+ for (const cb of keyCallbacks) {
1598
1620
  try {
1599
1621
  cb(event);
1600
1622
  } catch {
@@ -1630,10 +1652,11 @@ var FlagsClient = class {
1630
1652
  // ------------------------------------------------------------------
1631
1653
  // Internal: model conversion
1632
1654
  // ------------------------------------------------------------------
1655
+ /** @internal */
1633
1656
  _resourceToModel(resource) {
1634
1657
  const attrs = resource.attributes;
1635
1658
  return new Flag(this, {
1636
- id: resource.id ?? "",
1659
+ id: resource.id ?? null,
1637
1660
  key: attrs.key,
1638
1661
  name: attrs.name,
1639
1662
  type: attrs.type,
@@ -1657,13 +1680,567 @@ var FlagsClient = class {
1657
1680
  environments: attrs.environments ?? {}
1658
1681
  };
1659
1682
  }
1660
- _parseContextType(data) {
1661
- const attrs = data.attributes ?? {};
1662
- return new ContextType({
1663
- id: data.id ?? "",
1683
+ };
1684
+
1685
+ // src/logging/client.ts
1686
+ import createClient3 from "openapi-fetch";
1687
+
1688
+ // src/logging/models.ts
1689
+ var Logger = class {
1690
+ /** UUID of the logger, or `null` if unsaved. */
1691
+ id;
1692
+ /** Unique key (dot-separated hierarchy). */
1693
+ key;
1694
+ /** Human-readable display name. */
1695
+ name;
1696
+ /** Base log level, or null if inherited. */
1697
+ level;
1698
+ /** UUID of the parent log group, or null. */
1699
+ group;
1700
+ /** Whether this logger is managed by the platform. */
1701
+ managed;
1702
+ /** Observed sources (services that report this logger). */
1703
+ sources;
1704
+ /** Per-environment level overrides. */
1705
+ environments;
1706
+ /** When the logger was created. */
1707
+ createdAt;
1708
+ /** When the logger was last updated. */
1709
+ updatedAt;
1710
+ /** @internal */
1711
+ _client;
1712
+ /** @internal */
1713
+ constructor(client, fields) {
1714
+ this._client = client;
1715
+ this.id = fields.id;
1716
+ this.key = fields.key;
1717
+ this.name = fields.name;
1718
+ this.level = fields.level;
1719
+ this.group = fields.group;
1720
+ this.managed = fields.managed;
1721
+ this.sources = fields.sources;
1722
+ this.environments = fields.environments;
1723
+ this.createdAt = fields.createdAt;
1724
+ this.updatedAt = fields.updatedAt;
1725
+ }
1726
+ /**
1727
+ * Persist this logger to the server.
1728
+ *
1729
+ * POST if `id` is null (new), PUT if `id` is set (update).
1730
+ */
1731
+ async save() {
1732
+ const saved = await this._client._saveLogger(this);
1733
+ this._apply(saved);
1734
+ }
1735
+ /** Set the base log level (sync local mutation). */
1736
+ setLevel(level) {
1737
+ this.level = level;
1738
+ }
1739
+ /** Clear the base log level (sync local mutation). */
1740
+ clearLevel() {
1741
+ this.level = null;
1742
+ }
1743
+ /** Set an environment-specific log level (sync local mutation). */
1744
+ setEnvironmentLevel(env, level) {
1745
+ const envs = { ...this.environments };
1746
+ envs[env] = { ...envs[env] ?? {}, level };
1747
+ this.environments = envs;
1748
+ }
1749
+ /** Clear an environment-specific log level (sync local mutation). */
1750
+ clearEnvironmentLevel(env) {
1751
+ const envs = { ...this.environments };
1752
+ if (envs[env]) {
1753
+ const entry = { ...envs[env] };
1754
+ delete entry.level;
1755
+ envs[env] = entry;
1756
+ this.environments = envs;
1757
+ }
1758
+ }
1759
+ /** Clear all environment-specific log levels (sync local mutation). */
1760
+ clearAllEnvironmentLevels() {
1761
+ this.environments = {};
1762
+ }
1763
+ /** @internal — copy all fields from another Logger instance. */
1764
+ _apply(other) {
1765
+ this.id = other.id;
1766
+ this.key = other.key;
1767
+ this.name = other.name;
1768
+ this.level = other.level;
1769
+ this.group = other.group;
1770
+ this.managed = other.managed;
1771
+ this.sources = other.sources;
1772
+ this.environments = other.environments;
1773
+ this.createdAt = other.createdAt;
1774
+ this.updatedAt = other.updatedAt;
1775
+ }
1776
+ toString() {
1777
+ return `Logger(key=${this.key}, level=${this.level})`;
1778
+ }
1779
+ };
1780
+ var LogGroup = class {
1781
+ /** UUID of the log group, or `null` if unsaved. */
1782
+ id;
1783
+ /** Unique key. */
1784
+ key;
1785
+ /** Human-readable display name. */
1786
+ name;
1787
+ /** Base log level, or null if inherited. */
1788
+ level;
1789
+ /** UUID of the parent log group, or null. */
1790
+ group;
1791
+ /** Per-environment level overrides. */
1792
+ environments;
1793
+ /** When the log group was created. */
1794
+ createdAt;
1795
+ /** When the log group was last updated. */
1796
+ updatedAt;
1797
+ /** @internal */
1798
+ _client;
1799
+ /** @internal */
1800
+ constructor(client, fields) {
1801
+ this._client = client;
1802
+ this.id = fields.id;
1803
+ this.key = fields.key;
1804
+ this.name = fields.name;
1805
+ this.level = fields.level;
1806
+ this.group = fields.group;
1807
+ this.environments = fields.environments;
1808
+ this.createdAt = fields.createdAt;
1809
+ this.updatedAt = fields.updatedAt;
1810
+ }
1811
+ /**
1812
+ * Persist this log group to the server.
1813
+ *
1814
+ * POST if `id` is null (new), PUT if `id` is set (update).
1815
+ */
1816
+ async save() {
1817
+ const saved = await this._client._saveLogGroup(this);
1818
+ this._apply(saved);
1819
+ }
1820
+ /** Set the base log level (sync local mutation). */
1821
+ setLevel(level) {
1822
+ this.level = level;
1823
+ }
1824
+ /** Clear the base log level (sync local mutation). */
1825
+ clearLevel() {
1826
+ this.level = null;
1827
+ }
1828
+ /** Set an environment-specific log level (sync local mutation). */
1829
+ setEnvironmentLevel(env, level) {
1830
+ const envs = { ...this.environments };
1831
+ envs[env] = { ...envs[env] ?? {}, level };
1832
+ this.environments = envs;
1833
+ }
1834
+ /** Clear an environment-specific log level (sync local mutation). */
1835
+ clearEnvironmentLevel(env) {
1836
+ const envs = { ...this.environments };
1837
+ if (envs[env]) {
1838
+ const entry = { ...envs[env] };
1839
+ delete entry.level;
1840
+ envs[env] = entry;
1841
+ this.environments = envs;
1842
+ }
1843
+ }
1844
+ /** Clear all environment-specific log levels (sync local mutation). */
1845
+ clearAllEnvironmentLevels() {
1846
+ this.environments = {};
1847
+ }
1848
+ /** @internal — copy all fields from another LogGroup instance. */
1849
+ _apply(other) {
1850
+ this.id = other.id;
1851
+ this.key = other.key;
1852
+ this.name = other.name;
1853
+ this.level = other.level;
1854
+ this.group = other.group;
1855
+ this.environments = other.environments;
1856
+ this.createdAt = other.createdAt;
1857
+ this.updatedAt = other.updatedAt;
1858
+ }
1859
+ toString() {
1860
+ return `LogGroup(key=${this.key}, level=${this.level})`;
1861
+ }
1862
+ };
1863
+
1864
+ // src/logging/client.ts
1865
+ var LOGGING_BASE_URL = "https://logging.smplkit.com";
1866
+ async function checkError3(response, _context) {
1867
+ const body = await response.text().catch(() => "");
1868
+ throwForStatus(response.status, body);
1869
+ }
1870
+ function wrapFetchError3(err) {
1871
+ if (err instanceof SmplNotFoundError || err instanceof SmplConflictError || err instanceof SmplValidationError || err instanceof SmplError) {
1872
+ throw err;
1873
+ }
1874
+ if (err instanceof TypeError) {
1875
+ throw new SmplConnectionError(`Network error: ${err.message}`);
1876
+ }
1877
+ throw new SmplConnectionError(
1878
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1879
+ );
1880
+ }
1881
+ var LoggingClient = class {
1882
+ /** @internal */
1883
+ _apiKey;
1884
+ /** @internal */
1885
+ _baseUrl = LOGGING_BASE_URL;
1886
+ /** @internal */
1887
+ _http;
1888
+ /** @internal — set by SmplClient after construction. */
1889
+ _parent = null;
1890
+ _ensureWs;
1891
+ _wsManager = null;
1892
+ _started = false;
1893
+ _globalListeners = [];
1894
+ _keyListeners = /* @__PURE__ */ new Map();
1895
+ /** @internal */
1896
+ constructor(apiKey, ensureWs, timeout) {
1897
+ this._apiKey = apiKey;
1898
+ this._ensureWs = ensureWs;
1899
+ const ms = timeout ?? 3e4;
1900
+ this._http = createClient3({
1901
+ baseUrl: LOGGING_BASE_URL,
1902
+ headers: {
1903
+ Authorization: `Bearer ${apiKey}`,
1904
+ Accept: "application/json"
1905
+ },
1906
+ fetch: async (request) => {
1907
+ const controller = new AbortController();
1908
+ const timer = setTimeout(() => controller.abort(), ms);
1909
+ try {
1910
+ return await fetch(new Request(request, { signal: controller.signal }));
1911
+ } catch (err) {
1912
+ if (err instanceof DOMException && err.name === "AbortError") {
1913
+ throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1914
+ }
1915
+ throw err;
1916
+ } finally {
1917
+ clearTimeout(timer);
1918
+ }
1919
+ }
1920
+ });
1921
+ }
1922
+ // ------------------------------------------------------------------
1923
+ // Management: Logger factory
1924
+ // ------------------------------------------------------------------
1925
+ /** Create an unsaved logger. Call `.save()` to persist. */
1926
+ new(key, options) {
1927
+ return new Logger(this, {
1928
+ id: null,
1929
+ key,
1930
+ name: options?.name ?? keyToDisplayName(key),
1931
+ level: null,
1932
+ group: null,
1933
+ managed: options?.managed ?? false,
1934
+ sources: [],
1935
+ environments: {},
1936
+ createdAt: null,
1937
+ updatedAt: null
1938
+ });
1939
+ }
1940
+ // ------------------------------------------------------------------
1941
+ // Management: Logger CRUD
1942
+ // ------------------------------------------------------------------
1943
+ /** Fetch a logger by key. */
1944
+ async get(key) {
1945
+ let data;
1946
+ try {
1947
+ const result = await this._http.GET("/api/v1/loggers", {
1948
+ params: { query: { "filter[key]": key } }
1949
+ });
1950
+ if (result.error !== void 0)
1951
+ await checkError3(result.response, `Logger with key '${key}' not found`);
1952
+ data = result.data;
1953
+ } catch (err) {
1954
+ wrapFetchError3(err);
1955
+ }
1956
+ if (!data || !data.data || data.data.length === 0) {
1957
+ throw new SmplNotFoundError(`Logger with key '${key}' not found`);
1958
+ }
1959
+ return this._loggerToModel(data.data[0]);
1960
+ }
1961
+ /** List all loggers. */
1962
+ async list() {
1963
+ let data;
1964
+ try {
1965
+ const result = await this._http.GET("/api/v1/loggers", {});
1966
+ if (result.error !== void 0) await checkError3(result.response, "Failed to list loggers");
1967
+ data = result.data;
1968
+ } catch (err) {
1969
+ wrapFetchError3(err);
1970
+ }
1971
+ if (!data) return [];
1972
+ return data.data.map((r) => this._loggerToModel(r));
1973
+ }
1974
+ /** Delete a logger by key. */
1975
+ async delete(key) {
1976
+ const logger = await this.get(key);
1977
+ try {
1978
+ const result = await this._http.DELETE("/api/v1/loggers/{id}", {
1979
+ params: { path: { id: logger.id } }
1980
+ });
1981
+ if (result.error !== void 0 && result.response.status !== 204)
1982
+ await checkError3(result.response, `Failed to delete logger '${key}'`);
1983
+ } catch (err) {
1984
+ wrapFetchError3(err);
1985
+ }
1986
+ }
1987
+ // ------------------------------------------------------------------
1988
+ // Management: LogGroup factory
1989
+ // ------------------------------------------------------------------
1990
+ /** Create an unsaved log group. Call `.save()` to persist. */
1991
+ newGroup(key, options) {
1992
+ return new LogGroup(this, {
1993
+ id: null,
1994
+ key,
1995
+ name: options?.name ?? keyToDisplayName(key),
1996
+ level: null,
1997
+ group: options?.group ?? null,
1998
+ environments: {},
1999
+ createdAt: null,
2000
+ updatedAt: null
2001
+ });
2002
+ }
2003
+ // ------------------------------------------------------------------
2004
+ // Management: LogGroup CRUD
2005
+ // ------------------------------------------------------------------
2006
+ /** Fetch a log group by key. */
2007
+ async getGroup(key) {
2008
+ const groups = await this.listGroups();
2009
+ const match = groups.find((g) => g.key === key);
2010
+ if (!match) {
2011
+ throw new SmplNotFoundError(`LogGroup with key '${key}' not found`);
2012
+ }
2013
+ return match;
2014
+ }
2015
+ /** List all log groups. */
2016
+ async listGroups() {
2017
+ let data;
2018
+ try {
2019
+ const result = await this._http.GET("/api/v1/log_groups", {});
2020
+ if (result.error !== void 0)
2021
+ await checkError3(result.response, "Failed to list log groups");
2022
+ data = result.data;
2023
+ } catch (err) {
2024
+ wrapFetchError3(err);
2025
+ }
2026
+ if (!data) return [];
2027
+ return data.data.map((r) => this._groupToModel(r));
2028
+ }
2029
+ /** Delete a log group by key. */
2030
+ async deleteGroup(key) {
2031
+ const group = await this.getGroup(key);
2032
+ try {
2033
+ const result = await this._http.DELETE("/api/v1/log_groups/{id}", {
2034
+ params: { path: { id: group.id } }
2035
+ });
2036
+ if (result.error !== void 0 && result.response.status !== 204)
2037
+ await checkError3(result.response, `Failed to delete log group '${key}'`);
2038
+ } catch (err) {
2039
+ wrapFetchError3(err);
2040
+ }
2041
+ }
2042
+ // ------------------------------------------------------------------
2043
+ // Management: internal save methods
2044
+ // ------------------------------------------------------------------
2045
+ /** @internal — POST or PUT a logger. */
2046
+ async _saveLogger(logger) {
2047
+ const body = {
2048
+ data: {
2049
+ type: "logger",
2050
+ attributes: {
2051
+ key: logger.key,
2052
+ name: logger.name,
2053
+ level: logger.level,
2054
+ group: logger.group,
2055
+ managed: logger.managed,
2056
+ environments: logger.environments
2057
+ }
2058
+ }
2059
+ };
2060
+ if (logger.id === null) {
2061
+ let data;
2062
+ try {
2063
+ const result = await this._http.POST("/api/v1/loggers", { body });
2064
+ if (result.error !== void 0)
2065
+ await checkError3(result.response, "Failed to create logger");
2066
+ data = result.data;
2067
+ } catch (err) {
2068
+ wrapFetchError3(err);
2069
+ }
2070
+ if (!data || !data.data) throw new SmplValidationError("Failed to create logger");
2071
+ return this._loggerToModel(data.data);
2072
+ } else {
2073
+ let data;
2074
+ try {
2075
+ const result = await this._http.PUT("/api/v1/loggers/{id}", {
2076
+ params: { path: { id: logger.id } },
2077
+ body
2078
+ });
2079
+ if (result.error !== void 0)
2080
+ await checkError3(result.response, `Failed to update logger ${logger.id}`);
2081
+ data = result.data;
2082
+ } catch (err) {
2083
+ wrapFetchError3(err);
2084
+ }
2085
+ if (!data || !data.data)
2086
+ throw new SmplValidationError(`Failed to update logger ${logger.id}`);
2087
+ return this._loggerToModel(data.data);
2088
+ }
2089
+ }
2090
+ /** @internal — POST or PUT a log group. */
2091
+ async _saveLogGroup(group) {
2092
+ const body = {
2093
+ data: {
2094
+ type: "log_group",
2095
+ attributes: {
2096
+ key: group.key,
2097
+ name: group.name,
2098
+ level: group.level,
2099
+ group: group.group,
2100
+ environments: group.environments
2101
+ }
2102
+ }
2103
+ };
2104
+ if (group.id === null) {
2105
+ let data;
2106
+ try {
2107
+ const result = await this._http.POST("/api/v1/log_groups", { body });
2108
+ if (result.error !== void 0)
2109
+ await checkError3(result.response, "Failed to create log group");
2110
+ data = result.data;
2111
+ } catch (err) {
2112
+ wrapFetchError3(err);
2113
+ }
2114
+ if (!data || !data.data) throw new SmplValidationError("Failed to create log group");
2115
+ return this._groupToModel(data.data);
2116
+ } else {
2117
+ let data;
2118
+ try {
2119
+ const result = await this._http.PUT("/api/v1/log_groups/{id}", {
2120
+ params: { path: { id: group.id } },
2121
+ body
2122
+ });
2123
+ if (result.error !== void 0)
2124
+ await checkError3(result.response, `Failed to update log group ${group.id}`);
2125
+ data = result.data;
2126
+ } catch (err) {
2127
+ wrapFetchError3(err);
2128
+ }
2129
+ if (!data || !data.data)
2130
+ throw new SmplValidationError(`Failed to update log group ${group.id}`);
2131
+ return this._groupToModel(data.data);
2132
+ }
2133
+ }
2134
+ // ------------------------------------------------------------------
2135
+ // Runtime: start (scaffolded)
2136
+ // ------------------------------------------------------------------
2137
+ /**
2138
+ * Start the logging runtime.
2139
+ *
2140
+ * Fetches existing loggers/groups and wires WebSocket listeners for
2141
+ * live updates. Idempotent — safe to call multiple times.
2142
+ *
2143
+ * Note: Node.js auto-discovery (equivalent to Python's logging module
2144
+ * monkey-patching) is deferred. Management methods work without start().
2145
+ */
2146
+ async start() {
2147
+ if (this._started) return;
2148
+ this._wsManager = this._ensureWs();
2149
+ this._wsManager.on("logger_changed", this._handleLoggerChanged);
2150
+ this._started = true;
2151
+ }
2152
+ // ------------------------------------------------------------------
2153
+ // Runtime: change listeners (dual-mode)
2154
+ // ------------------------------------------------------------------
2155
+ /**
2156
+ * Register a change listener.
2157
+ *
2158
+ * - `onChange(callback)` — fires for any logger change (global).
2159
+ * - `onChange(key, callback)` — fires only for the specified logger key.
2160
+ */
2161
+ onChange(callbackOrKey, callback) {
2162
+ if (typeof callbackOrKey === "function") {
2163
+ this._globalListeners.push(callbackOrKey);
2164
+ } else {
2165
+ const key = callbackOrKey;
2166
+ if (!callback) {
2167
+ throw new SmplError("onChange(key, callback) requires a callback function.");
2168
+ }
2169
+ if (!this._keyListeners.has(key)) {
2170
+ this._keyListeners.set(key, []);
2171
+ }
2172
+ this._keyListeners.get(key).push(callback);
2173
+ }
2174
+ }
2175
+ // ------------------------------------------------------------------
2176
+ // Internal: close
2177
+ // ------------------------------------------------------------------
2178
+ /** @internal */
2179
+ _close() {
2180
+ if (this._wsManager !== null) {
2181
+ this._wsManager.off("logger_changed", this._handleLoggerChanged);
2182
+ this._wsManager = null;
2183
+ }
2184
+ this._started = false;
2185
+ }
2186
+ // ------------------------------------------------------------------
2187
+ // Internal: WebSocket handler
2188
+ // ------------------------------------------------------------------
2189
+ _handleLoggerChanged = (data) => {
2190
+ const key = data.key;
2191
+ if (key) {
2192
+ const level = data.level ?? null;
2193
+ const event = {
2194
+ key,
2195
+ level,
2196
+ source: "websocket"
2197
+ };
2198
+ for (const cb of this._globalListeners) {
2199
+ try {
2200
+ cb(event);
2201
+ } catch {
2202
+ }
2203
+ }
2204
+ const keyCallbacks = this._keyListeners.get(key);
2205
+ if (keyCallbacks) {
2206
+ for (const cb of keyCallbacks) {
2207
+ try {
2208
+ cb(event);
2209
+ } catch {
2210
+ }
2211
+ }
2212
+ }
2213
+ }
2214
+ };
2215
+ // ------------------------------------------------------------------
2216
+ // Internal: model conversion
2217
+ // ------------------------------------------------------------------
2218
+ _loggerToModel(resource) {
2219
+ const attrs = resource.attributes;
2220
+ return new Logger(this, {
2221
+ id: resource.id ?? null,
1664
2222
  key: attrs.key ?? "",
1665
- name: attrs.name ?? "",
1666
- attributes: attrs.attributes ?? {}
2223
+ name: attrs.name,
2224
+ level: attrs.level ?? null,
2225
+ group: attrs.group ?? null,
2226
+ managed: attrs.managed ?? false,
2227
+ sources: attrs.sources ?? [],
2228
+ environments: attrs.environments ?? {},
2229
+ createdAt: attrs.created_at ?? null,
2230
+ updatedAt: attrs.updated_at ?? null
2231
+ });
2232
+ }
2233
+ _groupToModel(resource) {
2234
+ const attrs = resource.attributes;
2235
+ return new LogGroup(this, {
2236
+ id: resource.id ?? null,
2237
+ key: attrs.key ?? "",
2238
+ name: attrs.name,
2239
+ level: attrs.level ?? null,
2240
+ group: attrs.group ?? null,
2241
+ environments: attrs.environments ?? {},
2242
+ createdAt: attrs.created_at ?? null,
2243
+ updatedAt: attrs.updated_at ?? null
1667
2244
  });
1668
2245
  }
1669
2246
  };
@@ -1879,17 +2456,18 @@ var APP_BASE_URL2 = "https://app.smplkit.com";
1879
2456
  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
2457
  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
2458
  var SmplClient = class {
1882
- /** Client for config management-plane operations. */
2459
+ /** Client for config management and runtime. */
1883
2460
  config;
1884
- /** Client for flags management and runtime operations. */
2461
+ /** Client for flags management and runtime. */
1885
2462
  flags;
2463
+ /** Client for logging management and runtime. */
2464
+ logging;
1886
2465
  _wsManager = null;
1887
2466
  _apiKey;
1888
2467
  /** @internal */
1889
2468
  _environment;
1890
2469
  /** @internal */
1891
2470
  _service;
1892
- _connected = false;
1893
2471
  _timeout;
1894
2472
  _appHttp;
1895
2473
  constructor(options = {}) {
@@ -1906,48 +2484,21 @@ var SmplClient = class {
1906
2484
  const apiKey = resolveApiKey(options.apiKey, environment);
1907
2485
  this._apiKey = apiKey;
1908
2486
  this._timeout = options.timeout ?? 3e4;
1909
- const ms = this._timeout;
1910
- this._appHttp = createClient3({
2487
+ this._appHttp = createClient4({
1911
2488
  baseUrl: APP_BASE_URL2,
1912
2489
  headers: {
1913
2490
  Authorization: `Bearer ${apiKey}`,
1914
2491
  Accept: "application/json"
1915
- },
1916
- fetch: async (request) => {
1917
- const controller = new AbortController();
1918
- const timer = setTimeout(() => controller.abort(), ms);
1919
- try {
1920
- return await fetch(new Request(request, { signal: controller.signal }));
1921
- } catch (err) {
1922
- if (err instanceof DOMException && err.name === "AbortError") {
1923
- throw new SmplTimeoutError(`Request timed out after ${ms}ms`);
1924
- }
1925
- throw err;
1926
- } finally {
1927
- clearTimeout(timer);
1928
- }
1929
2492
  }
1930
2493
  });
1931
2494
  this.config = new ConfigClient(apiKey, this._timeout);
1932
2495
  this.flags = new FlagsClient(apiKey, () => this._ensureWs(), this._timeout);
2496
+ this.logging = new LoggingClient(apiKey, () => this._ensureWs(), this._timeout);
1933
2497
  this.config._getSharedWs = () => this._ensureWs();
1934
2498
  this.flags._parent = this;
1935
2499
  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;
2500
+ this.logging._parent = this;
2501
+ void this._registerServiceContext();
1951
2502
  }
1952
2503
  /** @internal */
1953
2504
  async _registerServiceContext() {
@@ -1976,6 +2527,7 @@ var SmplClient = class {
1976
2527
  }
1977
2528
  /** Close the shared WebSocket and release resources. */
1978
2529
  close() {
2530
+ this.logging._close();
1979
2531
  if (this._wsManager !== null) {
1980
2532
  this._wsManager.stop();
1981
2533
  this._wsManager = null;
@@ -2047,28 +2599,43 @@ var Rule = class {
2047
2599
  return result;
2048
2600
  }
2049
2601
  };
2602
+
2603
+ // src/logging/types.ts
2604
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2605
+ LogLevel2["TRACE"] = "TRACE";
2606
+ LogLevel2["DEBUG"] = "DEBUG";
2607
+ LogLevel2["INFO"] = "INFO";
2608
+ LogLevel2["WARN"] = "WARN";
2609
+ LogLevel2["ERROR"] = "ERROR";
2610
+ LogLevel2["FATAL"] = "FATAL";
2611
+ LogLevel2["SILENT"] = "SILENT";
2612
+ return LogLevel2;
2613
+ })(LogLevel || {});
2050
2614
  export {
2051
- BoolFlagHandle,
2615
+ BooleanFlag,
2052
2616
  Config,
2053
2617
  ConfigClient,
2054
2618
  Context,
2055
- ContextType,
2056
2619
  Flag,
2057
2620
  FlagChangeEvent,
2058
2621
  FlagStats,
2059
2622
  FlagsClient,
2060
- JsonFlagHandle,
2061
- NumberFlagHandle,
2623
+ JsonFlag,
2624
+ LiveConfigProxy,
2625
+ LogGroup,
2626
+ LogLevel,
2627
+ Logger,
2628
+ LoggingClient,
2629
+ NumberFlag,
2062
2630
  Rule,
2063
2631
  SharedWebSocket,
2064
2632
  SmplClient,
2065
2633
  SmplConflictError,
2066
2634
  SmplConnectionError,
2067
2635
  SmplError,
2068
- SmplNotConnectedError,
2069
2636
  SmplNotFoundError,
2070
2637
  SmplTimeoutError,
2071
2638
  SmplValidationError,
2072
- StringFlagHandle
2639
+ StringFlag
2073
2640
  };
2074
2641
  //# sourceMappingURL=index.js.map