@smplkit/sdk 3.0.6 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -16623,6 +16623,7 @@ var index_exports = {};
16623
16623
  __export(index_exports, {
16624
16624
  AccountSettings: () => AccountSettings,
16625
16625
  AccountSettingsClient: () => AccountSettingsClient,
16626
+ AuditClient: () => AuditClient,
16626
16627
  BooleanFlag: () => BooleanFlag,
16627
16628
  Color: () => Color,
16628
16629
  Config: () => Config,
@@ -16681,6 +16682,264 @@ module.exports = __toCommonJS(index_exports);
16681
16682
  // src/client.ts
16682
16683
  var import_openapi_fetch5 = __toESM(require("openapi-fetch"), 1);
16683
16684
 
16685
+ // src/audit/buffer.ts
16686
+ var MAX_BUFFER_SIZE = 1e3;
16687
+ var PERIODIC_FLUSH_INTERVAL_MS = 5e3;
16688
+ var HIGH_WATERMARK = 50;
16689
+ var MAX_ATTEMPTS_PER_ITEM = 5;
16690
+ var INITIAL_BACKOFF_MS = 250;
16691
+ var MAX_BACKOFF_MS = 8e3;
16692
+ var AuditEventBuffer = class {
16693
+ _queue = [];
16694
+ _post;
16695
+ _maxSize;
16696
+ _watermark;
16697
+ _flushTimer;
16698
+ _draining = false;
16699
+ _closed = false;
16700
+ _droppedCount = 0;
16701
+ constructor(opts) {
16702
+ this._post = opts.post;
16703
+ this._maxSize = opts.maxSize ?? MAX_BUFFER_SIZE;
16704
+ this._watermark = opts.watermark ?? HIGH_WATERMARK;
16705
+ const interval = opts.flushIntervalMs ?? PERIODIC_FLUSH_INTERVAL_MS;
16706
+ this._flushTimer = setInterval(() => {
16707
+ void this._drainOnce();
16708
+ }, interval);
16709
+ if (typeof this._flushTimer.unref === "function") {
16710
+ this._flushTimer.unref();
16711
+ }
16712
+ }
16713
+ /** Enqueue a new event. May evict the oldest queued item if full. */
16714
+ enqueue(body, idempotencyKey = null) {
16715
+ if (this._closed) return;
16716
+ if (this._queue.length >= this._maxSize) {
16717
+ this._queue.shift();
16718
+ this._droppedCount += 1;
16719
+ console.warn(
16720
+ `[smplkit.audit] buffer full (size=${this._maxSize}); dropped oldest event (total dropped=${this._droppedCount})`
16721
+ );
16722
+ }
16723
+ this._queue.push({ body, idempotencyKey, attempts: 0, nextRetryAt: 0 });
16724
+ if (this._queue.length >= this._watermark) {
16725
+ void this._drainOnce();
16726
+ }
16727
+ }
16728
+ /** Block (cooperatively) until the buffer is empty or `timeoutMs` elapses. */
16729
+ async flush(timeoutMs = 5e3) {
16730
+ const deadline = Date.now() + timeoutMs;
16731
+ while (this._queue.length > 0) {
16732
+ if (Date.now() >= deadline) {
16733
+ console.warn(`[smplkit.audit] flush timed out after ${timeoutMs}ms`);
16734
+ return;
16735
+ }
16736
+ void this._drainOnce();
16737
+ await new Promise((r) => setTimeout(r, 50));
16738
+ }
16739
+ }
16740
+ /** Stop the periodic timer, drain best-effort, and mark closed. */
16741
+ async close(timeoutMs = 5e3) {
16742
+ this._closed = true;
16743
+ await this.flush(timeoutMs);
16744
+ if (this._flushTimer !== null) {
16745
+ clearInterval(this._flushTimer);
16746
+ this._flushTimer = null;
16747
+ }
16748
+ }
16749
+ async _drainOnce() {
16750
+ if (this._draining) return;
16751
+ this._draining = true;
16752
+ try {
16753
+ const now = Date.now();
16754
+ while (this._queue.length > 0) {
16755
+ const head = this._queue[0];
16756
+ if (head.nextRetryAt > now) break;
16757
+ this._queue.shift();
16758
+ let outcome;
16759
+ try {
16760
+ outcome = await this._post(head);
16761
+ } catch (err) {
16762
+ outcome = { status: 0 };
16763
+ }
16764
+ const requeued = this._handleOutcome(head, outcome);
16765
+ if (requeued !== null) {
16766
+ this._queue.unshift(requeued);
16767
+ break;
16768
+ }
16769
+ }
16770
+ } finally {
16771
+ this._draining = false;
16772
+ }
16773
+ }
16774
+ _handleOutcome(item, outcome) {
16775
+ if (outcome.status >= 200 && outcome.status < 300) return null;
16776
+ if (outcome.status >= 400 && outcome.status < 500 && outcome.status !== 429) {
16777
+ console.warn(`[smplkit.audit] permanent failure status=${outcome.status}; event dropped`);
16778
+ return null;
16779
+ }
16780
+ item.attempts += 1;
16781
+ if (item.attempts >= MAX_ATTEMPTS_PER_ITEM) {
16782
+ console.warn(
16783
+ `[smplkit.audit] gave up after ${item.attempts} attempts (last_status=${outcome.status})`
16784
+ );
16785
+ return null;
16786
+ }
16787
+ const backoff = Math.min(MAX_BACKOFF_MS, INITIAL_BACKOFF_MS * 2 ** (item.attempts - 1));
16788
+ const jitter = Math.random() * backoff * 0.25;
16789
+ item.nextRetryAt = Date.now() + backoff + jitter;
16790
+ return item;
16791
+ }
16792
+ };
16793
+
16794
+ // src/audit/client.ts
16795
+ var JSONAPI_HEADERS = {
16796
+ "Content-Type": "application/vnd.api+json",
16797
+ Accept: "application/vnd.api+json"
16798
+ };
16799
+ function _attributesFromInput(input) {
16800
+ const attrs = {
16801
+ action: input.action,
16802
+ resource_type: input.resourceType,
16803
+ resource_id: input.resourceId
16804
+ };
16805
+ if (input.occurredAt !== void 0) {
16806
+ const ts = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : input.occurredAt;
16807
+ attrs.occurred_at = ts;
16808
+ }
16809
+ if (input.snapshot !== void 0) attrs.snapshot = input.snapshot;
16810
+ if (input.data !== void 0) attrs.data = input.data;
16811
+ return attrs;
16812
+ }
16813
+ function _eventFromResource(resource) {
16814
+ const attrs = resource.attributes;
16815
+ return {
16816
+ id: resource.id,
16817
+ action: String(attrs.action ?? ""),
16818
+ resourceType: String(attrs.resource_type ?? ""),
16819
+ resourceId: String(attrs.resource_id ?? ""),
16820
+ occurredAt: String(attrs.occurred_at ?? ""),
16821
+ createdAt: String(attrs.created_at ?? ""),
16822
+ actorType: String(attrs.actor_type ?? ""),
16823
+ actorId: attrs.actor_id ?? null,
16824
+ actorLabel: String(attrs.actor_label ?? ""),
16825
+ snapshot: attrs.snapshot ?? null,
16826
+ data: attrs.data ?? {},
16827
+ idempotencyKey: String(attrs.idempotency_key ?? "")
16828
+ };
16829
+ }
16830
+ var EventsClient = class {
16831
+ _apiKey;
16832
+ _baseUrl;
16833
+ _timeoutMs;
16834
+ _buffer;
16835
+ /** @internal */
16836
+ _fetch = fetch;
16837
+ constructor(opts) {
16838
+ this._apiKey = opts.apiKey;
16839
+ this._baseUrl = opts.baseUrl.replace(/\/$/, "");
16840
+ this._timeoutMs = opts.timeoutMs ?? 1e4;
16841
+ this._buffer = new AuditEventBuffer({
16842
+ post: async (item) => {
16843
+ try {
16844
+ const headers = {
16845
+ ...JSONAPI_HEADERS,
16846
+ Authorization: `Bearer ${this._apiKey}`
16847
+ };
16848
+ if (item.idempotencyKey !== null) headers["Idempotency-Key"] = item.idempotencyKey;
16849
+ const ctrl = new AbortController();
16850
+ const t = setTimeout(() => ctrl.abort(), this._timeoutMs);
16851
+ try {
16852
+ const resp = await this._fetch(`${this._baseUrl}/api/v1/events`, {
16853
+ method: "POST",
16854
+ headers,
16855
+ body: JSON.stringify(item.body),
16856
+ signal: ctrl.signal
16857
+ });
16858
+ return { status: resp.status };
16859
+ } finally {
16860
+ clearTimeout(t);
16861
+ }
16862
+ } catch {
16863
+ return { status: 0 };
16864
+ }
16865
+ }
16866
+ });
16867
+ }
16868
+ /**
16869
+ * Enqueue an audit event for asynchronous delivery.
16870
+ * Returns immediately. The actual POST happens on the buffer worker.
16871
+ *
16872
+ * Customers may not emit `smpl.*` resource types — the server will
16873
+ * reject those with a 403 (the buffer logs and drops permanent
16874
+ * failures, so a misuse will silently disappear from the queue).
16875
+ */
16876
+ create(input) {
16877
+ const body = {
16878
+ data: { type: "event", attributes: _attributesFromInput(input) }
16879
+ };
16880
+ this._buffer.enqueue(body, input.idempotencyKey ?? null);
16881
+ }
16882
+ async list(params = {}) {
16883
+ const qs = new URLSearchParams();
16884
+ if (params.action !== void 0) qs.set("filter[action]", params.action);
16885
+ if (params.resourceType !== void 0) qs.set("filter[resource_type]", params.resourceType);
16886
+ if (params.resourceId !== void 0) qs.set("filter[resource_id]", params.resourceId);
16887
+ if (params.actorType !== void 0) qs.set("filter[actor_type]", params.actorType);
16888
+ if (params.actorId !== void 0) qs.set("filter[actor_id]", params.actorId);
16889
+ if (params.occurredAtRange !== void 0) qs.set("filter[occurred_at]", params.occurredAtRange);
16890
+ if (params.pageSize !== void 0) qs.set("page[size]", String(params.pageSize));
16891
+ if (params.pageAfter !== void 0) qs.set("page[after]", params.pageAfter);
16892
+ const resp = await this._fetch(`${this._baseUrl}/api/v1/events?${qs.toString()}`, {
16893
+ headers: {
16894
+ Authorization: `Bearer ${this._apiKey}`,
16895
+ Accept: "application/vnd.api+json"
16896
+ }
16897
+ });
16898
+ if (!resp.ok) {
16899
+ throw new Error(`audit list failed: ${resp.status} ${resp.statusText}`);
16900
+ }
16901
+ const body = await resp.json();
16902
+ const events = (body.data ?? []).map(_eventFromResource);
16903
+ let nextCursor = null;
16904
+ const nextLink = body.links?.next;
16905
+ if (typeof nextLink === "string" && nextLink.includes("page[after]=")) {
16906
+ nextCursor = nextLink.split("page[after]=")[1];
16907
+ }
16908
+ return { events, nextCursor };
16909
+ }
16910
+ async get(eventId) {
16911
+ const resp = await this._fetch(`${this._baseUrl}/api/v1/events/${eventId}`, {
16912
+ headers: {
16913
+ Authorization: `Bearer ${this._apiKey}`,
16914
+ Accept: "application/vnd.api+json"
16915
+ }
16916
+ });
16917
+ if (!resp.ok) {
16918
+ throw new Error(`audit get failed: ${resp.status} ${resp.statusText}`);
16919
+ }
16920
+ const body = await resp.json();
16921
+ return _eventFromResource(body.data);
16922
+ }
16923
+ /** Block until the in-memory buffer is drained or `timeoutMs` elapses. */
16924
+ async flush(timeoutMs = 5e3) {
16925
+ await this._buffer.flush(timeoutMs);
16926
+ }
16927
+ /** @internal */
16928
+ async _close() {
16929
+ await this._buffer.close();
16930
+ }
16931
+ };
16932
+ var AuditClient = class {
16933
+ events;
16934
+ constructor(opts) {
16935
+ this.events = new EventsClient(opts);
16936
+ }
16937
+ /** @internal */
16938
+ async _close() {
16939
+ await this.events._close();
16940
+ }
16941
+ };
16942
+
16684
16943
  // src/config/client.ts
16685
16944
  var import_openapi_fetch = __toESM(require("openapi-fetch"), 1);
16686
16945
 
@@ -21957,6 +22216,8 @@ var SmplClient = class {
21957
22216
  flags;
21958
22217
  /** Client for logging management and runtime. */
21959
22218
  logging;
22219
+ /** Client for the audit service — fire-and-forget event recording (ADR-047). */
22220
+ audit;
21960
22221
  /**
21961
22222
  * Standalone management/CRUD entry point — mirrors Python's
21962
22223
  * `client.manage`. Construction is side-effect-free; safe to use even
@@ -21987,6 +22248,7 @@ var SmplClient = class {
21987
22248
  const configBaseUrl = serviceUrl(cfg.scheme, "config", cfg.baseDomain);
21988
22249
  const flagsBaseUrl = serviceUrl(cfg.scheme, "flags", cfg.baseDomain);
21989
22250
  const loggingBaseUrl = serviceUrl(cfg.scheme, "logging", cfg.baseDomain);
22251
+ const auditBaseUrl = serviceUrl(cfg.scheme, "audit", cfg.baseDomain);
21990
22252
  this._appBaseUrl = appBaseUrl;
21991
22253
  const maskedKey = cfg.apiKey.length > 14 ? cfg.apiKey.slice(0, 10) + "..." + cfg.apiKey.slice(-4) : cfg.apiKey.slice(0, Math.min(4, cfg.apiKey.length)) + "...";
21992
22254
  debug(
@@ -22029,6 +22291,11 @@ var SmplClient = class {
22029
22291
  this._timeout,
22030
22292
  loggingBaseUrl
22031
22293
  );
22294
+ this.audit = new AuditClient({
22295
+ apiKey: cfg.apiKey,
22296
+ baseUrl: auditBaseUrl,
22297
+ timeoutMs: this._timeout
22298
+ });
22032
22299
  this.config._getSharedWs = () => this._ensureWs();
22033
22300
  this.flags._parent = this;
22034
22301
  this.config._parent = this;
@@ -22108,6 +22375,7 @@ var SmplClient = class {
22108
22375
  }
22109
22376
  this.flags._close();
22110
22377
  this.logging._close();
22378
+ void this.audit._close();
22111
22379
  if (this._wsManager !== null) {
22112
22380
  this._wsManager.stop();
22113
22381
  this._wsManager = null;
@@ -22342,6 +22610,7 @@ var PinoAdapter = class {
22342
22610
  0 && (module.exports = {
22343
22611
  AccountSettings,
22344
22612
  AccountSettingsClient,
22613
+ AuditClient,
22345
22614
  BooleanFlag,
22346
22615
  Color,
22347
22616
  Config,