@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 +269 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -1
- package/dist/index.d.ts +99 -1
- package/dist/index.js +268 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|