@liteguard/core 0.2.20260314
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/README.md +36 -0
- package/dist/index.d.mts +729 -0
- package/dist/index.d.ts +729 -0
- package/dist/index.js +1372 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1343 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BaseLiteguardClient: () => BaseLiteguardClient,
|
|
24
|
+
LiteguardScope: () => LiteguardScope,
|
|
25
|
+
evaluateGuard: () => evaluateGuard
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/evaluation.ts
|
|
30
|
+
function evaluateGuard(guard, properties) {
|
|
31
|
+
for (const rule of guard.rules) {
|
|
32
|
+
if (!rule.enabled) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (matchesRule(rule, properties)) {
|
|
36
|
+
return rule.result;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return guard.defaultValue;
|
|
40
|
+
}
|
|
41
|
+
function matchesRule(rule, properties) {
|
|
42
|
+
const raw = properties[rule.propertyName];
|
|
43
|
+
if (raw === void 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (rule.values.length === 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
switch (rule.operator) {
|
|
50
|
+
case "EQUALS":
|
|
51
|
+
return valuesEqual(raw, rule.values[0]);
|
|
52
|
+
case "NOT_EQUALS":
|
|
53
|
+
return !valuesEqual(raw, rule.values[0]);
|
|
54
|
+
case "IN":
|
|
55
|
+
return rule.values.some((value) => valuesEqual(raw, value));
|
|
56
|
+
case "NOT_IN":
|
|
57
|
+
return rule.values.every((value) => !valuesEqual(raw, value));
|
|
58
|
+
case "REGEX": {
|
|
59
|
+
const pattern = String(rule.values[0] ?? "");
|
|
60
|
+
try {
|
|
61
|
+
return new RegExp(pattern).test(String(raw));
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
case "GT":
|
|
67
|
+
return (compareOrdered(raw, rule.values[0]) ?? 0) > 0;
|
|
68
|
+
case "GTE":
|
|
69
|
+
return (compareOrdered(raw, rule.values[0]) ?? -1) >= 0;
|
|
70
|
+
case "LT":
|
|
71
|
+
return (compareOrdered(raw, rule.values[0]) ?? 0) < 0;
|
|
72
|
+
case "LTE":
|
|
73
|
+
return (compareOrdered(raw, rule.values[0]) ?? 1) <= 0;
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function valuesEqual(a, b) {
|
|
79
|
+
if (b === void 0) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (typeof a === "boolean" || typeof b === "boolean") {
|
|
83
|
+
return typeof a === "boolean" && typeof b === "boolean" && a === b;
|
|
84
|
+
}
|
|
85
|
+
if (typeof a === "string" || typeof b === "string") {
|
|
86
|
+
return typeof a === "string" && typeof b === "string" && a === b;
|
|
87
|
+
}
|
|
88
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
89
|
+
return a === b;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function compareOrdered(a, b) {
|
|
94
|
+
if (b === void 0) {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
if (typeof a === "string" || typeof b === "string") {
|
|
98
|
+
if (typeof a !== "string" || typeof b !== "string") {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
return a.localeCompare(b);
|
|
102
|
+
}
|
|
103
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
104
|
+
return a - b;
|
|
105
|
+
}
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/client-base.ts
|
|
110
|
+
var DEFAULT_BACKEND_URL = "https://api.liteguard.io";
|
|
111
|
+
var DEFAULT_REFRESH_RATE_S = 30;
|
|
112
|
+
var DEFAULT_FLUSH_RATE_S = 10;
|
|
113
|
+
var DEFAULT_FLUSH_SIZE = 500;
|
|
114
|
+
var DEFAULT_HTTP_TIMEOUT_S = 4;
|
|
115
|
+
var DEFAULT_FLUSH_BUFFER_MULTIPLIER = 4;
|
|
116
|
+
var DEFAULT_QUIET = true;
|
|
117
|
+
var PUBLIC_BUNDLE_KEY = "";
|
|
118
|
+
function positiveNumberOrDefault(value, fallback) {
|
|
119
|
+
return value !== void 0 && value > 0 ? value : fallback;
|
|
120
|
+
}
|
|
121
|
+
function nonEmptyStringOrDefault(value, fallback) {
|
|
122
|
+
return value !== void 0 && value.trim() !== "" ? value : fallback;
|
|
123
|
+
}
|
|
124
|
+
function cloneProperties(properties) {
|
|
125
|
+
return { ...properties };
|
|
126
|
+
}
|
|
127
|
+
function cloneProtectedContext(protectedContext) {
|
|
128
|
+
if (!protectedContext) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
signature: protectedContext.signature,
|
|
133
|
+
properties: { ...protectedContext.properties ?? {} }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function protectedContextCacheKey(protectedContext) {
|
|
137
|
+
if (!protectedContext) {
|
|
138
|
+
return PUBLIC_BUNDLE_KEY;
|
|
139
|
+
}
|
|
140
|
+
const keys = Object.keys(protectedContext.properties ?? {}).sort();
|
|
141
|
+
const parts = [protectedContext.signature, ""];
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
parts.push(`${key}=${protectedContext.properties?.[key] ?? ""}`);
|
|
144
|
+
}
|
|
145
|
+
return parts.join("\0");
|
|
146
|
+
}
|
|
147
|
+
function isPromiseLike(value) {
|
|
148
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
149
|
+
}
|
|
150
|
+
function asyncContextUnsupportedError(operation) {
|
|
151
|
+
return new Error(
|
|
152
|
+
`[liteguard] ${operation} cannot cross await boundaries in this runtime. Use an explicit LiteguardScope and call scope methods directly after each await.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
var signalCounter = 0;
|
|
156
|
+
var LiteguardScope = class {
|
|
157
|
+
client;
|
|
158
|
+
snapshot;
|
|
159
|
+
/** @internal — use {@link BaseLiteguardClient.createScope} instead. */
|
|
160
|
+
constructor(client, snapshot) {
|
|
161
|
+
this.client = client;
|
|
162
|
+
this.snapshot = {
|
|
163
|
+
properties: cloneProperties(snapshot.properties),
|
|
164
|
+
protectedBundleKey: snapshot.protectedBundleKey,
|
|
165
|
+
protectedContext: cloneProtectedContext(snapshot.protectedContext)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Derive a new scope with the given properties merged on top of this
|
|
170
|
+
* scope's existing properties. Does not mutate the current scope.
|
|
171
|
+
*
|
|
172
|
+
* @param properties - Key/value pairs to merge into the new scope.
|
|
173
|
+
* @returns A new {@link LiteguardScope} with the merged properties.
|
|
174
|
+
*/
|
|
175
|
+
withProperties(properties) {
|
|
176
|
+
return this.client._deriveScope(this, {
|
|
177
|
+
properties: {
|
|
178
|
+
...this.snapshot.properties,
|
|
179
|
+
...properties
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Alias for {@link withProperties} — derives a new scope with additional properties.
|
|
185
|
+
*
|
|
186
|
+
* @param properties - Key/value pairs to add.
|
|
187
|
+
* @returns A new {@link LiteguardScope} with the merged properties.
|
|
188
|
+
*/
|
|
189
|
+
addProperties(properties) {
|
|
190
|
+
return this.withProperties(properties);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Derive a new scope with the specified property keys removed.
|
|
194
|
+
*
|
|
195
|
+
* @param names - Property keys to remove.
|
|
196
|
+
* @returns A new {@link LiteguardScope} without the listed properties.
|
|
197
|
+
*/
|
|
198
|
+
clearProperties(names) {
|
|
199
|
+
const next = cloneProperties(this.snapshot.properties);
|
|
200
|
+
for (const name of names) {
|
|
201
|
+
delete next[name];
|
|
202
|
+
}
|
|
203
|
+
return this.client._deriveScope(this, { properties: next });
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Derive a new scope with all properties removed.
|
|
207
|
+
*
|
|
208
|
+
* @returns A new {@link LiteguardScope} with an empty property bag.
|
|
209
|
+
*/
|
|
210
|
+
resetProperties() {
|
|
211
|
+
return this.client._deriveScope(this, { properties: {} });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Derive a new scope bound to the supplied protected context. The client
|
|
215
|
+
* will fetch (or reuse) a guard bundle specific to this context.
|
|
216
|
+
*
|
|
217
|
+
* @param protectedContext - Signed context bundle from your auth backend.
|
|
218
|
+
* @returns A new {@link LiteguardScope} bound to the protected context.
|
|
219
|
+
*/
|
|
220
|
+
async bindProtectedContext(protectedContext) {
|
|
221
|
+
return await this.client._bindProtectedContextToScope(this, protectedContext);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Derive a new scope with protected context cleared, reverting to the
|
|
225
|
+
* public guard bundle.
|
|
226
|
+
*
|
|
227
|
+
* @returns A new {@link LiteguardScope} using the public guard bundle.
|
|
228
|
+
*/
|
|
229
|
+
clearProtectedContext() {
|
|
230
|
+
return this.client._deriveScope(this, {
|
|
231
|
+
protectedBundleKey: PUBLIC_BUNDLE_KEY,
|
|
232
|
+
protectedContext: null
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check whether the named guard is open within this scope. Buffers a
|
|
237
|
+
* `guard_check` telemetry signal.
|
|
238
|
+
*
|
|
239
|
+
* @param name - Guard name (e.g. `"payments.checkout"`).
|
|
240
|
+
* @param options - Optional per-call overrides (extra properties, fallback).
|
|
241
|
+
* @returns `true` if the guard is open, `false` otherwise.
|
|
242
|
+
*/
|
|
243
|
+
isOpen(name, options = {}) {
|
|
244
|
+
return this.client.isOpen(name, { ...options, scope: this });
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check whether the named guard is open without emitting a telemetry
|
|
248
|
+
* signal or consuming a rate-limit slot. Useful in hot paths or render
|
|
249
|
+
* loops where you only need the boolean result.
|
|
250
|
+
*
|
|
251
|
+
* @param name - Guard name to evaluate.
|
|
252
|
+
* @param options - Optional per-call overrides.
|
|
253
|
+
* @returns `true` if the guard is open, `false` otherwise.
|
|
254
|
+
*/
|
|
255
|
+
peekIsOpen(name, options = {}) {
|
|
256
|
+
return this.client.peekIsOpen(name, { ...options, scope: this });
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Evaluate the guard and, if open, call `fn` within a correlated execution
|
|
260
|
+
* scope. Returns `fn`'s result when open, or `undefined` when closed.
|
|
261
|
+
*
|
|
262
|
+
* Emits both a `guard_check` and a `guard_execution` telemetry signal so
|
|
263
|
+
* you can measure the guarded code path.
|
|
264
|
+
*
|
|
265
|
+
* @param name - Guard name to evaluate.
|
|
266
|
+
* @param fn - Synchronous function to invoke when the guard is open.
|
|
267
|
+
* @param options - Optional per-call overrides.
|
|
268
|
+
* @returns The return value of `fn`, or `undefined` if the guard is closed.
|
|
269
|
+
*/
|
|
270
|
+
executeIfOpen(name, fn, options = {}) {
|
|
271
|
+
return this.client.executeIfOpen(name, fn, { ...options, scope: this });
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Async variant of {@link executeIfOpen}. Evaluates the guard and, if open,
|
|
275
|
+
* awaits `fn` within a correlated execution scope.
|
|
276
|
+
*
|
|
277
|
+
* @param name - Guard name to evaluate.
|
|
278
|
+
* @param fn - Async function to invoke when the guard is open.
|
|
279
|
+
* @param options - Optional per-call overrides.
|
|
280
|
+
* @returns The resolved value of `fn`, or `undefined` if the guard is closed.
|
|
281
|
+
*/
|
|
282
|
+
async executeIfOpenAsync(name, fn, options = {}) {
|
|
283
|
+
return await this.client.executeIfOpenAsync(name, fn, { ...options, scope: this });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Run `fn` with this scope as the active scope for all guard checks made
|
|
287
|
+
* during its execution.
|
|
288
|
+
*
|
|
289
|
+
* @param fn - Callback to execute within this scope.
|
|
290
|
+
* @returns The return value of `fn`.
|
|
291
|
+
*/
|
|
292
|
+
run(fn) {
|
|
293
|
+
return this.client.runWithScope(this, fn);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Run `fn` with this scope active **and** inside a Liteguard execution
|
|
297
|
+
* context so that all guard signals share a common execution ID.
|
|
298
|
+
*
|
|
299
|
+
* @param fn - Callback to execute.
|
|
300
|
+
* @returns The return value of `fn`.
|
|
301
|
+
*/
|
|
302
|
+
withExecution(fn) {
|
|
303
|
+
return this.client.runWithScope(this, () => this.client.withExecution(fn));
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Return a shallow copy of this scope's properties.
|
|
307
|
+
*
|
|
308
|
+
* @returns A plain object of property key/value pairs.
|
|
309
|
+
*/
|
|
310
|
+
getProperties() {
|
|
311
|
+
return cloneProperties(this.snapshot.properties);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Return the protected context bound to this scope, or `null` if none.
|
|
315
|
+
*
|
|
316
|
+
* @returns A copy of the protected context, or `null`.
|
|
317
|
+
*/
|
|
318
|
+
getProtectedContext() {
|
|
319
|
+
return cloneProtectedContext(this.snapshot.protectedContext);
|
|
320
|
+
}
|
|
321
|
+
_getBundleKey() {
|
|
322
|
+
return this.snapshot.protectedBundleKey;
|
|
323
|
+
}
|
|
324
|
+
_getSnapshot() {
|
|
325
|
+
return {
|
|
326
|
+
properties: cloneProperties(this.snapshot.properties),
|
|
327
|
+
protectedBundleKey: this.snapshot.protectedBundleKey,
|
|
328
|
+
protectedContext: cloneProtectedContext(this.snapshot.protectedContext)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
_belongsTo(client) {
|
|
332
|
+
return this.client === client;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var BaseLiteguardClient = class {
|
|
336
|
+
projectClientKeyId;
|
|
337
|
+
runtime;
|
|
338
|
+
options;
|
|
339
|
+
bundles = /* @__PURE__ */ new Map();
|
|
340
|
+
defaultScope;
|
|
341
|
+
signalBuffer = [];
|
|
342
|
+
droppedSignalsPending = 0;
|
|
343
|
+
reportedUnadoptedGuards = /* @__PURE__ */ new Set();
|
|
344
|
+
pendingUnadoptedGuards = /* @__PURE__ */ new Set();
|
|
345
|
+
refreshTimer = null;
|
|
346
|
+
flushTimer = null;
|
|
347
|
+
lifecycleCleanup = null;
|
|
348
|
+
currentRefreshRateSeconds;
|
|
349
|
+
rateLimitState = /* @__PURE__ */ new Map();
|
|
350
|
+
listeners = /* @__PURE__ */ new Set();
|
|
351
|
+
/**
|
|
352
|
+
* Create a new Liteguard client.
|
|
353
|
+
*
|
|
354
|
+
* Call {@link start} after construction to fetch the initial guard bundle
|
|
355
|
+
* and begin background timers.
|
|
356
|
+
*
|
|
357
|
+
* @param runtime - Platform adapter providing timers, fetch, and context storage.
|
|
358
|
+
* @param projectClientKeyId - Your project client key ID from the Liteguard dashboard.
|
|
359
|
+
* @param options - Optional SDK configuration overrides.
|
|
360
|
+
*/
|
|
361
|
+
constructor(runtime, projectClientKeyId, options = {}) {
|
|
362
|
+
this.runtime = runtime;
|
|
363
|
+
this.projectClientKeyId = projectClientKeyId;
|
|
364
|
+
this.options = {
|
|
365
|
+
environment: options.environment ?? "",
|
|
366
|
+
fallback: options.fallback ?? false,
|
|
367
|
+
refreshRateSeconds: positiveNumberOrDefault(options.refreshRateSeconds, DEFAULT_REFRESH_RATE_S),
|
|
368
|
+
flushRateSeconds: positiveNumberOrDefault(options.flushRateSeconds, DEFAULT_FLUSH_RATE_S),
|
|
369
|
+
flushSize: positiveNumberOrDefault(options.flushSize, DEFAULT_FLUSH_SIZE),
|
|
370
|
+
httpTimeoutSeconds: positiveNumberOrDefault(options.httpTimeoutSeconds, DEFAULT_HTTP_TIMEOUT_S),
|
|
371
|
+
flushBufferMultiplier: positiveNumberOrDefault(
|
|
372
|
+
options.flushBufferMultiplier,
|
|
373
|
+
DEFAULT_FLUSH_BUFFER_MULTIPLIER
|
|
374
|
+
),
|
|
375
|
+
disableMeasurement: options.disableMeasurement ?? false,
|
|
376
|
+
backendUrl: nonEmptyStringOrDefault(options.backendUrl, DEFAULT_BACKEND_URL),
|
|
377
|
+
quiet: options.quiet ?? DEFAULT_QUIET
|
|
378
|
+
};
|
|
379
|
+
this.currentRefreshRateSeconds = this.options.refreshRateSeconds;
|
|
380
|
+
this.bundles.set(PUBLIC_BUNDLE_KEY, this.createEmptyBundle(PUBLIC_BUNDLE_KEY, null));
|
|
381
|
+
this.defaultScope = new LiteguardScope(this, {
|
|
382
|
+
properties: {},
|
|
383
|
+
protectedBundleKey: PUBLIC_BUNDLE_KEY,
|
|
384
|
+
protectedContext: null
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Fetch the initial public guard bundle and start background refresh and
|
|
389
|
+
* flush timers. Must be called once after construction before using the
|
|
390
|
+
* client in production.
|
|
391
|
+
*/
|
|
392
|
+
async start() {
|
|
393
|
+
await this.fetchGuardsForBundle(PUBLIC_BUNDLE_KEY);
|
|
394
|
+
this.scheduleNextRefresh();
|
|
395
|
+
this.flushTimer = this.runtime.setInterval(() => {
|
|
396
|
+
void this.flushSignals();
|
|
397
|
+
}, this.options.flushRateSeconds * 1e3);
|
|
398
|
+
this.runtime.detachTimer?.(this.flushTimer);
|
|
399
|
+
this.lifecycleCleanup = this.runtime.installLifecycleHooks?.({
|
|
400
|
+
flushKeepalive: () => {
|
|
401
|
+
void this.flushSignals({ keepalive: true });
|
|
402
|
+
}
|
|
403
|
+
}) ?? null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Stop all background timers and flush remaining buffered signals.
|
|
407
|
+
* After calling `shutdown` the client should not be used further.
|
|
408
|
+
*/
|
|
409
|
+
async shutdown() {
|
|
410
|
+
if (this.refreshTimer !== null) {
|
|
411
|
+
this.runtime.clearTimeout(this.refreshTimer);
|
|
412
|
+
this.refreshTimer = null;
|
|
413
|
+
}
|
|
414
|
+
if (this.flushTimer !== null) {
|
|
415
|
+
this.runtime.clearInterval(this.flushTimer);
|
|
416
|
+
this.flushTimer = null;
|
|
417
|
+
}
|
|
418
|
+
this.lifecycleCleanup?.();
|
|
419
|
+
this.lifecycleCleanup = null;
|
|
420
|
+
await this.flushSignals();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Returns `true` once the initial public guard bundle has been fetched.
|
|
424
|
+
*/
|
|
425
|
+
isReady() {
|
|
426
|
+
return this.getBundle(PUBLIC_BUNDLE_KEY)?.ready ?? false;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Register a listener that is called whenever the guard bundle is refreshed.
|
|
430
|
+
*
|
|
431
|
+
* @param listener - Callback invoked on each guard bundle update.
|
|
432
|
+
* @returns An unsubscribe function — call it to remove the listener.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* const unsubscribe = client.subscribe(() => {
|
|
437
|
+
* console.log('Guards refreshed');
|
|
438
|
+
* });
|
|
439
|
+
* // later…
|
|
440
|
+
* unsubscribe();
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
subscribe(listener) {
|
|
444
|
+
this.listeners.add(listener);
|
|
445
|
+
return () => {
|
|
446
|
+
this.listeners.delete(listener);
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Create a new request scope with optional initial properties. The scope
|
|
451
|
+
* uses the public guard bundle by default; call
|
|
452
|
+
* {@link LiteguardScope.bindProtectedContext} on the returned scope to
|
|
453
|
+
* attach signed user context.
|
|
454
|
+
*
|
|
455
|
+
* @param properties - Initial property key/value pairs for the scope.
|
|
456
|
+
* @returns A new {@link LiteguardScope}.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* const scope = client.createScope({ plan: 'enterprise' });
|
|
461
|
+
* scope.isOpen('feature.beta'); // evaluates with plan=enterprise
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
createScope(properties = {}) {
|
|
465
|
+
return new LiteguardScope(this, {
|
|
466
|
+
properties: cloneProperties(properties),
|
|
467
|
+
protectedBundleKey: PUBLIC_BUNDLE_KEY,
|
|
468
|
+
protectedContext: null
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Return the currently active request scope, or the shared default scope
|
|
473
|
+
* when no request-local scope is active.
|
|
474
|
+
*
|
|
475
|
+
* On Node.js, the active scope is resolved from `AsyncLocalStorage` so
|
|
476
|
+
* each concurrent request can carry its own context automatically.
|
|
477
|
+
*
|
|
478
|
+
* @returns The active {@link LiteguardScope}.
|
|
479
|
+
*/
|
|
480
|
+
getActiveScope() {
|
|
481
|
+
const active = this.getRuntimeScope();
|
|
482
|
+
return active ?? this.defaultScope;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Run `fn` inside the given request scope. All guard checks performed
|
|
486
|
+
* during `fn` will use the properties and protected context of `scope`.
|
|
487
|
+
*
|
|
488
|
+
* @param scope - The scope to activate.
|
|
489
|
+
* @param fn - Callback to execute within the scope.
|
|
490
|
+
* @returns The return value of `fn`.
|
|
491
|
+
*/
|
|
492
|
+
runWithScope(scope, fn) {
|
|
493
|
+
const resolvedScope = this.resolveScope(scope);
|
|
494
|
+
const adapter = this.runtime.requestScope;
|
|
495
|
+
if (adapter) {
|
|
496
|
+
const result = adapter.run({ currentScope: resolvedScope }, fn);
|
|
497
|
+
if (isPromiseLike(result) && !this.runtime.supportsAsyncContextPropagation) {
|
|
498
|
+
throw asyncContextUnsupportedError("scope.run(), client.runWithScope(), and property-scoped callbacks");
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
const previousScope = this.defaultScope;
|
|
503
|
+
this.defaultScope = resolvedScope;
|
|
504
|
+
try {
|
|
505
|
+
const result = fn();
|
|
506
|
+
if (isPromiseLike(result)) {
|
|
507
|
+
if (!this.runtime.supportsAsyncContextPropagation) {
|
|
508
|
+
throw asyncContextUnsupportedError("scope.run(), client.runWithScope(), and property-scoped callbacks");
|
|
509
|
+
}
|
|
510
|
+
return Promise.resolve(result).finally(() => {
|
|
511
|
+
this.defaultScope = previousScope;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
this.defaultScope = previousScope;
|
|
515
|
+
return result;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
this.defaultScope = previousScope;
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Convenience helper that derives a scope from the current active scope,
|
|
523
|
+
* merges in the supplied properties, and runs `fn` inside it.
|
|
524
|
+
*
|
|
525
|
+
* @param properties - Key/value pairs to add for the duration of `fn`.
|
|
526
|
+
* @param fn - Callback to execute within the scoped context.
|
|
527
|
+
* @returns The return value of `fn`.
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* const result = client.withProperties({ userId: '42' }, () => {
|
|
532
|
+
* return client.isOpen('feature.beta');
|
|
533
|
+
* });
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
withProperties(properties, fn) {
|
|
537
|
+
const scope = this.getActiveScope().withProperties(properties);
|
|
538
|
+
return this.runWithScope(scope, fn);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Bind protected context to a derived scope and run `fn` inside it.
|
|
542
|
+
* Fetches (or reuses) a guard bundle specific to this context before
|
|
543
|
+
* executing `fn`.
|
|
544
|
+
*
|
|
545
|
+
* @param protectedContext - Signed context bundle from your auth backend.
|
|
546
|
+
* @param fn - Callback to execute within the protected scope.
|
|
547
|
+
* @returns The (awaited) return value of `fn`.
|
|
548
|
+
*/
|
|
549
|
+
async withProtectedContext(protectedContext, fn) {
|
|
550
|
+
const scope = await this.getActiveScope().bindProtectedContext(protectedContext);
|
|
551
|
+
return await Promise.resolve(this.runWithScope(scope, fn));
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Run `fn` inside a Liteguard execution scope so that all guard signals
|
|
555
|
+
* emitted during `fn` share a common execution ID for correlated
|
|
556
|
+
* telemetry. If an execution scope is already active, `fn` is called
|
|
557
|
+
* directly without creating a new one.
|
|
558
|
+
*
|
|
559
|
+
* @param fn - Callback to execute within the execution scope.
|
|
560
|
+
* @returns The return value of `fn`.
|
|
561
|
+
*/
|
|
562
|
+
withExecution(fn) {
|
|
563
|
+
const existing = this.runtime.execution.getStore();
|
|
564
|
+
if (existing) {
|
|
565
|
+
const result2 = fn();
|
|
566
|
+
if (isPromiseLike(result2) && !this.runtime.supportsAsyncContextPropagation) {
|
|
567
|
+
throw asyncContextUnsupportedError("withExecution()");
|
|
568
|
+
}
|
|
569
|
+
return result2;
|
|
570
|
+
}
|
|
571
|
+
const result = this.runtime.execution.run({ executionId: nextSignalId(), sequenceNumber: 0 }, fn);
|
|
572
|
+
if (isPromiseLike(result) && !this.runtime.supportsAsyncContextPropagation) {
|
|
573
|
+
throw asyncContextUnsupportedError("withExecution()");
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Return `true` if the named guard is open for the resolved request scope.
|
|
579
|
+
* Buffers a `guard_check` telemetry signal and consumes a rate-limit slot
|
|
580
|
+
* when applicable.
|
|
581
|
+
*
|
|
582
|
+
* Guards that have not yet been adopted on the Liteguard dashboard always
|
|
583
|
+
* return `true`, allowing you to instrument code before enabling the guard.
|
|
584
|
+
*
|
|
585
|
+
* @param name - Guard name (e.g. `"payments.checkout"`).
|
|
586
|
+
* @param callOptions - Optional per-call overrides (extra properties, fallback, scope).
|
|
587
|
+
* @returns `true` if the guard is open, `false` otherwise.
|
|
588
|
+
*/
|
|
589
|
+
isOpen(name, callOptions = {}) {
|
|
590
|
+
return this.evaluateIsOpen(name, callOptions, true);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Signal-free variant of {@link isOpen}. Returns the same boolean result
|
|
594
|
+
* but does **not** emit a telemetry signal or consume a rate-limit slot.
|
|
595
|
+
* Ideal for render loops or other hot paths where you only need the
|
|
596
|
+
* boolean value.
|
|
597
|
+
*
|
|
598
|
+
* @param name - Guard name to evaluate.
|
|
599
|
+
* @param callOptions - Optional per-call overrides.
|
|
600
|
+
* @returns `true` if the guard is open, `false` otherwise.
|
|
601
|
+
*/
|
|
602
|
+
peekIsOpen(name, callOptions = {}) {
|
|
603
|
+
return this.evaluateIsOpen(name, callOptions, false);
|
|
604
|
+
}
|
|
605
|
+
evaluateIsOpen(name, callOptions, emitSignal) {
|
|
606
|
+
const scope = this.resolveScope(callOptions.scope);
|
|
607
|
+
const options = {
|
|
608
|
+
disableMeasurement: false,
|
|
609
|
+
...callOptions
|
|
610
|
+
};
|
|
611
|
+
const fallback = options.fallback ?? this.options.fallback;
|
|
612
|
+
const bundle = this.bundleForScope(scope);
|
|
613
|
+
if (!bundle.ready) {
|
|
614
|
+
return fallback;
|
|
615
|
+
}
|
|
616
|
+
const guard = bundle.guards.get(name);
|
|
617
|
+
if (!guard) {
|
|
618
|
+
this.recordUnadoptedGuard(name);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
if (!guard.adopted) {
|
|
622
|
+
this.recordUnadoptedGuard(name);
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
const props = {
|
|
626
|
+
...scope.getProperties(),
|
|
627
|
+
...options.properties ?? {}
|
|
628
|
+
};
|
|
629
|
+
let result = evaluateGuard(guard, props);
|
|
630
|
+
if (result && guard.rateLimitPerMinute > 0) {
|
|
631
|
+
result = emitSignal ? this.checkRateLimit(name, guard.rateLimitPerMinute, guard.rateLimitProperties, props) : this.wouldPassRateLimit(name, guard.rateLimitPerMinute, guard.rateLimitProperties, props);
|
|
632
|
+
}
|
|
633
|
+
if (emitSignal) {
|
|
634
|
+
this.bufferSignal({
|
|
635
|
+
guardName: name,
|
|
636
|
+
result,
|
|
637
|
+
properties: props,
|
|
638
|
+
callsiteId: this.captureCallsiteId(),
|
|
639
|
+
kind: "guard_check",
|
|
640
|
+
measurement: this.isMeasurementEnabled(guard, options) ? this.runtime.measurement.captureGuardCheck() : void 0
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Evaluate the guard and, if open, call `fn` inside a correlated execution
|
|
647
|
+
* scope. Returns `fn`'s result when the guard is open, or `undefined` when
|
|
648
|
+
* closed. Emits correlated `guard_check` and `guard_execution` signals.
|
|
649
|
+
*
|
|
650
|
+
* @param name - Guard name to evaluate.
|
|
651
|
+
* @param fn - Synchronous function to invoke when the guard is open.
|
|
652
|
+
* @param callOptions - Optional per-call overrides.
|
|
653
|
+
* @returns The return value of `fn`, or `undefined` if the guard is closed.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```ts
|
|
657
|
+
* const banner = client.executeIfOpen('promo.banner', () => {
|
|
658
|
+
* return renderPromoBanner();
|
|
659
|
+
* });
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
executeIfOpen(name, fn, callOptions = {}) {
|
|
663
|
+
const scope = this.resolveScope(callOptions.scope);
|
|
664
|
+
return this.runWithScope(
|
|
665
|
+
scope,
|
|
666
|
+
() => this.withExecution(() => {
|
|
667
|
+
const options = {
|
|
668
|
+
disableMeasurement: false,
|
|
669
|
+
...callOptions
|
|
670
|
+
};
|
|
671
|
+
const bundle = this.bundleForScope(scope);
|
|
672
|
+
const guard = bundle.guards.get(name);
|
|
673
|
+
const props = {
|
|
674
|
+
...scope.getProperties(),
|
|
675
|
+
...options.properties ?? {}
|
|
676
|
+
};
|
|
677
|
+
if (!this.isOpen(name, { ...options, scope })) {
|
|
678
|
+
return void 0;
|
|
679
|
+
}
|
|
680
|
+
if (!guard?.adopted) {
|
|
681
|
+
return fn();
|
|
682
|
+
}
|
|
683
|
+
const guardCheckSignalId = this.runtime.execution.getStore()?.lastSignalId;
|
|
684
|
+
const measurementEnabled = this.isMeasurementEnabled(guard, options);
|
|
685
|
+
const start = measurementEnabled ? this.runtime.measurement.beginGuardExecution() : void 0;
|
|
686
|
+
try {
|
|
687
|
+
const value = fn();
|
|
688
|
+
this.bufferSignal({
|
|
689
|
+
guardName: name,
|
|
690
|
+
result: true,
|
|
691
|
+
properties: props,
|
|
692
|
+
callsiteId: this.captureCallsiteId(),
|
|
693
|
+
kind: "guard_execution",
|
|
694
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, true) : void 0,
|
|
695
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
696
|
+
});
|
|
697
|
+
return value;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
this.bufferSignal({
|
|
700
|
+
guardName: name,
|
|
701
|
+
result: true,
|
|
702
|
+
properties: props,
|
|
703
|
+
callsiteId: this.captureCallsiteId(),
|
|
704
|
+
kind: "guard_execution",
|
|
705
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, false, error) : void 0,
|
|
706
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
707
|
+
});
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Async variant of {@link executeIfOpen}. Evaluates the guard and, if
|
|
715
|
+
* open, awaits `fn` inside a correlated execution scope.
|
|
716
|
+
*
|
|
717
|
+
* @param name - Guard name to evaluate.
|
|
718
|
+
* @param fn - Async function to invoke when the guard is open.
|
|
719
|
+
* @param callOptions - Optional per-call overrides.
|
|
720
|
+
* @returns The resolved value of `fn`, or `undefined` if the guard is closed.
|
|
721
|
+
*/
|
|
722
|
+
async executeIfOpenAsync(name, fn, callOptions = {}) {
|
|
723
|
+
const scope = this.resolveScope(callOptions.scope);
|
|
724
|
+
if (!this.runtime.supportsAsyncContextPropagation) {
|
|
725
|
+
return await this.executeIfOpenAsyncWithoutAsyncContext(name, scope, fn, callOptions);
|
|
726
|
+
}
|
|
727
|
+
return await this.runWithScope(
|
|
728
|
+
scope,
|
|
729
|
+
() => this.withExecution(async () => {
|
|
730
|
+
const options = {
|
|
731
|
+
disableMeasurement: false,
|
|
732
|
+
...callOptions
|
|
733
|
+
};
|
|
734
|
+
const bundle = this.bundleForScope(scope);
|
|
735
|
+
const guard = bundle.guards.get(name);
|
|
736
|
+
const props = {
|
|
737
|
+
...scope.getProperties(),
|
|
738
|
+
...options.properties ?? {}
|
|
739
|
+
};
|
|
740
|
+
if (!this.isOpen(name, { ...options, scope })) {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
if (!guard?.adopted) {
|
|
744
|
+
return await fn();
|
|
745
|
+
}
|
|
746
|
+
const guardCheckSignalId = this.runtime.execution.getStore()?.lastSignalId;
|
|
747
|
+
const measurementEnabled = this.isMeasurementEnabled(guard, options);
|
|
748
|
+
const start = measurementEnabled ? this.runtime.measurement.beginGuardExecution() : void 0;
|
|
749
|
+
try {
|
|
750
|
+
const value = await fn();
|
|
751
|
+
this.bufferSignal({
|
|
752
|
+
guardName: name,
|
|
753
|
+
result: true,
|
|
754
|
+
properties: props,
|
|
755
|
+
callsiteId: this.captureCallsiteId(),
|
|
756
|
+
kind: "guard_execution",
|
|
757
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, true) : void 0,
|
|
758
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
759
|
+
});
|
|
760
|
+
return value;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
this.bufferSignal({
|
|
763
|
+
guardName: name,
|
|
764
|
+
result: true,
|
|
765
|
+
properties: props,
|
|
766
|
+
callsiteId: this.captureCallsiteId(),
|
|
767
|
+
kind: "guard_execution",
|
|
768
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, false, error) : void 0,
|
|
769
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
770
|
+
});
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
async executeIfOpenAsyncWithoutAsyncContext(name, scope, fn, callOptions) {
|
|
777
|
+
const options = {
|
|
778
|
+
disableMeasurement: false,
|
|
779
|
+
...callOptions
|
|
780
|
+
};
|
|
781
|
+
const bundle = this.bundleForScope(scope);
|
|
782
|
+
const guard = bundle.guards.get(name);
|
|
783
|
+
const props = {
|
|
784
|
+
...scope.getProperties(),
|
|
785
|
+
...options.properties ?? {}
|
|
786
|
+
};
|
|
787
|
+
const executionState = this.runtime.execution.getStore() ?? { executionId: nextSignalId(), sequenceNumber: 0 };
|
|
788
|
+
const isOpen = this.runWithExplicitExecutionState(executionState, () => this.isOpen(name, { ...options, scope }));
|
|
789
|
+
if (!isOpen) {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
if (!guard?.adopted) {
|
|
793
|
+
return await fn();
|
|
794
|
+
}
|
|
795
|
+
const guardCheckSignalId = executionState.lastSignalId;
|
|
796
|
+
const measurementEnabled = this.isMeasurementEnabled(guard, options);
|
|
797
|
+
const start = measurementEnabled ? this.runtime.measurement.beginGuardExecution() : void 0;
|
|
798
|
+
try {
|
|
799
|
+
const value = await fn();
|
|
800
|
+
this.runWithExplicitExecutionState(executionState, () => {
|
|
801
|
+
this.bufferSignal({
|
|
802
|
+
guardName: name,
|
|
803
|
+
result: true,
|
|
804
|
+
properties: props,
|
|
805
|
+
callsiteId: this.captureCallsiteId(),
|
|
806
|
+
kind: "guard_execution",
|
|
807
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, true) : void 0,
|
|
808
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
return value;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
this.runWithExplicitExecutionState(executionState, () => {
|
|
814
|
+
this.bufferSignal({
|
|
815
|
+
guardName: name,
|
|
816
|
+
result: true,
|
|
817
|
+
properties: props,
|
|
818
|
+
callsiteId: this.captureCallsiteId(),
|
|
819
|
+
kind: "guard_execution",
|
|
820
|
+
measurement: measurementEnabled ? this.runtime.measurement.captureGuardExecution(start, false, error) : void 0,
|
|
821
|
+
parentSignalIdOverride: guardCheckSignalId
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Merge `properties` into the active scope's property bag and persist
|
|
829
|
+
* the derived scope as the new active scope. Subsequent {@link isOpen}
|
|
830
|
+
* calls will include these properties unless overridden per-call.
|
|
831
|
+
*
|
|
832
|
+
* @param properties - Key/value pairs to add.
|
|
833
|
+
* @returns The new active {@link LiteguardScope}.
|
|
834
|
+
*/
|
|
835
|
+
addProperties(properties) {
|
|
836
|
+
const nextScope = this.getActiveScope().withProperties(properties);
|
|
837
|
+
return this.replaceCurrentScope(nextScope);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Remove the given property keys from the active scope and persist the
|
|
841
|
+
* resulting scope.
|
|
842
|
+
*
|
|
843
|
+
* @param names - Property keys to remove.
|
|
844
|
+
* @returns The new active {@link LiteguardScope}.
|
|
845
|
+
*/
|
|
846
|
+
clearProperties(names) {
|
|
847
|
+
const nextScope = this.getActiveScope().clearProperties(names);
|
|
848
|
+
return this.replaceCurrentScope(nextScope);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Remove all properties from the active scope and persist the resulting
|
|
852
|
+
* scope.
|
|
853
|
+
*
|
|
854
|
+
* @returns The new active {@link LiteguardScope} with an empty property bag.
|
|
855
|
+
*/
|
|
856
|
+
resetProperties() {
|
|
857
|
+
const nextScope = this.getActiveScope().resetProperties();
|
|
858
|
+
return this.replaceCurrentScope(nextScope);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Bind protected context to the active scope and persist the derived
|
|
862
|
+
* scope. Fetches (or reuses) a guard bundle specific to this context.
|
|
863
|
+
*
|
|
864
|
+
* @param protectedContext - Signed context bundle from your auth backend.
|
|
865
|
+
* @returns The new active {@link LiteguardScope}.
|
|
866
|
+
*/
|
|
867
|
+
async bindProtectedContext(protectedContext) {
|
|
868
|
+
const nextScope = await this.getActiveScope().bindProtectedContext(protectedContext);
|
|
869
|
+
return this.replaceCurrentScope(nextScope);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Clear protected context from the active scope and revert to the
|
|
873
|
+
* public guard bundle.
|
|
874
|
+
*
|
|
875
|
+
* @returns The new active {@link LiteguardScope}.
|
|
876
|
+
*/
|
|
877
|
+
clearProtectedContext() {
|
|
878
|
+
const nextScope = this.getActiveScope().clearProtectedContext();
|
|
879
|
+
return this.replaceCurrentScope(nextScope);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Immediately transmit all buffered telemetry signals to the backend. Also
|
|
883
|
+
* flushes any pending unadopted-guard reports.
|
|
884
|
+
*
|
|
885
|
+
* Call this before process exit or page unload to avoid losing signals.
|
|
886
|
+
*
|
|
887
|
+
* @param options - Flush options (e.g. `{ keepalive: true }` for page unload).
|
|
888
|
+
*/
|
|
889
|
+
async flushSignals(options = {}) {
|
|
890
|
+
const signalBatch = this.signalBuffer.splice(0);
|
|
891
|
+
const unadoptedGuardNames = [...this.pendingUnadoptedGuards];
|
|
892
|
+
this.pendingUnadoptedGuards.clear();
|
|
893
|
+
if (signalBatch.length > 0) {
|
|
894
|
+
try {
|
|
895
|
+
const url = new URL("/api/v1/signals", this.options.backendUrl);
|
|
896
|
+
const body = JSON.stringify({
|
|
897
|
+
projectClientKeyId: this.projectClientKeyId,
|
|
898
|
+
environment: this.options.environment,
|
|
899
|
+
signals: signalBatch
|
|
900
|
+
});
|
|
901
|
+
await this.post(url, body, options.keepalive);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
this.log("[liteguard] Signal flush failed", error);
|
|
904
|
+
this.signalBuffer.unshift(...signalBatch);
|
|
905
|
+
const maxBuf = this.options.flushSize * this.options.flushBufferMultiplier;
|
|
906
|
+
if (this.signalBuffer.length > maxBuf) {
|
|
907
|
+
this.droppedSignalsPending += this.signalBuffer.length - maxBuf;
|
|
908
|
+
this.signalBuffer.length = maxBuf;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (unadoptedGuardNames.length > 0) {
|
|
913
|
+
try {
|
|
914
|
+
const url = new URL("/api/v1/unadopted-guards", this.options.backendUrl);
|
|
915
|
+
const requestBody = {
|
|
916
|
+
projectClientKeyId: this.projectClientKeyId,
|
|
917
|
+
environment: this.options.environment,
|
|
918
|
+
guardNames: unadoptedGuardNames
|
|
919
|
+
};
|
|
920
|
+
await this.post(url, JSON.stringify(requestBody), options.keepalive);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
this.log("[liteguard] Unadopted guard flush failed", error);
|
|
923
|
+
for (const guardName of unadoptedGuardNames) {
|
|
924
|
+
this.pendingUnadoptedGuards.add(guardName);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/** Notify subscribers that client-visible state has changed. */
|
|
930
|
+
emitChange() {
|
|
931
|
+
for (const listener of this.listeners) {
|
|
932
|
+
listener();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/** @internal Derive a new immutable scope from an existing scope snapshot. */
|
|
936
|
+
_deriveScope(scope, patch) {
|
|
937
|
+
const resolved = this.resolveScope(scope);
|
|
938
|
+
const snapshot = resolved._getSnapshot();
|
|
939
|
+
return new LiteguardScope(this, {
|
|
940
|
+
properties: patch.properties ? cloneProperties(patch.properties) : snapshot.properties,
|
|
941
|
+
protectedBundleKey: patch.protectedBundleKey ?? snapshot.protectedBundleKey,
|
|
942
|
+
protectedContext: patch.protectedContext === void 0 ? snapshot.protectedContext : cloneProtectedContext(patch.protectedContext)
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
/** @internal Bind protected context to a scope and ensure its bundle is loaded. */
|
|
946
|
+
async _bindProtectedContextToScope(scope, protectedContext) {
|
|
947
|
+
this.resolveScope(scope);
|
|
948
|
+
const clonedProtectedContext = cloneProtectedContext(protectedContext);
|
|
949
|
+
const bundleKey = await this.ensureBundleForProtectedContext(clonedProtectedContext);
|
|
950
|
+
return new LiteguardScope(this, {
|
|
951
|
+
properties: scope.getProperties(),
|
|
952
|
+
protectedBundleKey: bundleKey,
|
|
953
|
+
protectedContext: clonedProtectedContext
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** Persist a derived scope into the current request-local or default scope slot. */
|
|
957
|
+
replaceCurrentScope(nextScope) {
|
|
958
|
+
const resolved = this.resolveScope(nextScope);
|
|
959
|
+
const store = this.runtime.requestScope?.getStore();
|
|
960
|
+
if (store) {
|
|
961
|
+
store.currentScope = resolved;
|
|
962
|
+
return resolved;
|
|
963
|
+
}
|
|
964
|
+
this.defaultScope = resolved;
|
|
965
|
+
this.emitChange();
|
|
966
|
+
return resolved;
|
|
967
|
+
}
|
|
968
|
+
/** Resolve the active runtime scope store into a client-owned scope instance. */
|
|
969
|
+
getRuntimeScope() {
|
|
970
|
+
const store = this.runtime.requestScope?.getStore();
|
|
971
|
+
if (!store) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const scope = store.currentScope;
|
|
975
|
+
if (scope instanceof LiteguardScope && scope._belongsTo(this)) {
|
|
976
|
+
return scope;
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
/** Validate and resolve the scope used for a client operation. */
|
|
981
|
+
resolveScope(scope) {
|
|
982
|
+
const resolved = scope ?? this.getRuntimeScope() ?? this.defaultScope;
|
|
983
|
+
if (!resolved._belongsTo(this)) {
|
|
984
|
+
throw new Error("[liteguard] Scope belongs to a different Liteguard client.");
|
|
985
|
+
}
|
|
986
|
+
return resolved;
|
|
987
|
+
}
|
|
988
|
+
/** Queue a one-time report for a guard that exists only in application code. */
|
|
989
|
+
recordUnadoptedGuard(name) {
|
|
990
|
+
if (!this.reportedUnadoptedGuards.has(name)) {
|
|
991
|
+
this.reportedUnadoptedGuards.add(name);
|
|
992
|
+
this.pendingUnadoptedGuards.add(name);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/** Schedule the next periodic guard refresh using the current refresh interval. */
|
|
996
|
+
scheduleNextRefresh() {
|
|
997
|
+
if (this.refreshTimer !== null) {
|
|
998
|
+
this.runtime.clearTimeout(this.refreshTimer);
|
|
999
|
+
}
|
|
1000
|
+
this.refreshTimer = this.runtime.setTimeout(() => {
|
|
1001
|
+
void this.runRefreshCycle();
|
|
1002
|
+
}, this.currentRefreshRateSeconds * 1e3);
|
|
1003
|
+
this.runtime.detachTimer?.(this.refreshTimer);
|
|
1004
|
+
}
|
|
1005
|
+
/** Refresh every cached bundle once, then reschedule the next cycle. */
|
|
1006
|
+
async runRefreshCycle() {
|
|
1007
|
+
if (this.refreshTimer === null) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const bundleKeys = [...this.bundles.keys()].sort();
|
|
1011
|
+
for (const bundleKey of bundleKeys) {
|
|
1012
|
+
await this.fetchGuardsForBundle(bundleKey);
|
|
1013
|
+
}
|
|
1014
|
+
if (this.refreshTimer !== null) {
|
|
1015
|
+
this.scheduleNextRefresh();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/** Create an empty bundle placeholder before the first successful fetch. */
|
|
1019
|
+
createEmptyBundle(bundleKey, protectedContext) {
|
|
1020
|
+
return {
|
|
1021
|
+
key: bundleKey,
|
|
1022
|
+
guards: /* @__PURE__ */ new Map(),
|
|
1023
|
+
ready: false,
|
|
1024
|
+
etag: "",
|
|
1025
|
+
protectedContext: cloneProtectedContext(protectedContext),
|
|
1026
|
+
refreshRateSeconds: this.options.refreshRateSeconds
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/** Return a cached bundle by key, if present. */
|
|
1030
|
+
getBundle(bundleKey) {
|
|
1031
|
+
return this.bundles.get(bundleKey);
|
|
1032
|
+
}
|
|
1033
|
+
/** Resolve the bundle used for a scope, falling back to the public bundle. */
|
|
1034
|
+
bundleForScope(scope) {
|
|
1035
|
+
return this.getBundle(scope._getBundleKey()) ?? this.getBundle(PUBLIC_BUNDLE_KEY) ?? this.createEmptyBundle(PUBLIC_BUNDLE_KEY, null);
|
|
1036
|
+
}
|
|
1037
|
+
/** Ensure the bundle for a protected context exists locally and is ready. */
|
|
1038
|
+
async ensureBundleForProtectedContext(protectedContext) {
|
|
1039
|
+
const bundleKey = protectedContextCacheKey(protectedContext);
|
|
1040
|
+
const existing = this.getBundle(bundleKey);
|
|
1041
|
+
if (existing?.ready) {
|
|
1042
|
+
return bundleKey;
|
|
1043
|
+
}
|
|
1044
|
+
if (!existing) {
|
|
1045
|
+
this.bundles.set(bundleKey, this.createEmptyBundle(bundleKey, protectedContext));
|
|
1046
|
+
}
|
|
1047
|
+
await this.fetchGuardsForBundle(bundleKey);
|
|
1048
|
+
return bundleKey;
|
|
1049
|
+
}
|
|
1050
|
+
/** Use the shortest bundle refresh rate so all cached bundles stay current. */
|
|
1051
|
+
recomputeRefreshInterval() {
|
|
1052
|
+
let next = 0;
|
|
1053
|
+
for (const bundle of this.bundles.values()) {
|
|
1054
|
+
if (bundle.refreshRateSeconds <= 0) {
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (next === 0 || bundle.refreshRateSeconds < next) {
|
|
1058
|
+
next = bundle.refreshRateSeconds;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (next <= 0) {
|
|
1062
|
+
next = this.options.refreshRateSeconds;
|
|
1063
|
+
}
|
|
1064
|
+
if (next <= 0) {
|
|
1065
|
+
next = DEFAULT_REFRESH_RATE_S;
|
|
1066
|
+
}
|
|
1067
|
+
this.currentRefreshRateSeconds = next;
|
|
1068
|
+
}
|
|
1069
|
+
/** Fetch the latest guard bundle for a bundle key, respecting ETags and timeouts. */
|
|
1070
|
+
async fetchGuardsForBundle(bundleKey) {
|
|
1071
|
+
const bundle = this.getBundle(bundleKey) ?? this.getBundle(PUBLIC_BUNDLE_KEY);
|
|
1072
|
+
const etag = bundle?.etag ?? "";
|
|
1073
|
+
const protectedContext = cloneProtectedContext(bundle?.protectedContext ?? null);
|
|
1074
|
+
const { signal, cleanup } = this.createTimeoutSignal(this.options.httpTimeoutSeconds * 1e3);
|
|
1075
|
+
try {
|
|
1076
|
+
const url = new URL("/api/v1/guards", this.options.backendUrl);
|
|
1077
|
+
const headers = {
|
|
1078
|
+
Authorization: `Bearer ${this.projectClientKeyId}`,
|
|
1079
|
+
"Content-Type": "application/json"
|
|
1080
|
+
};
|
|
1081
|
+
if (etag) {
|
|
1082
|
+
headers["If-None-Match"] = etag;
|
|
1083
|
+
}
|
|
1084
|
+
const requestBody = {
|
|
1085
|
+
projectClientKeyId: this.projectClientKeyId,
|
|
1086
|
+
environment: this.options.environment,
|
|
1087
|
+
...protectedContext ? { protectedContext } : {}
|
|
1088
|
+
};
|
|
1089
|
+
const res = await this.runtime.fetch(url.toString(), {
|
|
1090
|
+
method: "POST",
|
|
1091
|
+
headers,
|
|
1092
|
+
body: JSON.stringify(requestBody),
|
|
1093
|
+
signal
|
|
1094
|
+
});
|
|
1095
|
+
if (res.status === 304) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (!res.ok) {
|
|
1099
|
+
await res.text();
|
|
1100
|
+
this.log(`[liteguard] Guard fetch failed: ${res.status} ${res.statusText}`);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const body = await res.json();
|
|
1104
|
+
const guards = /* @__PURE__ */ new Map();
|
|
1105
|
+
for (const guard of body.guards) {
|
|
1106
|
+
guards.set(guard.name, guard);
|
|
1107
|
+
}
|
|
1108
|
+
const nextBundle = this.getBundle(bundleKey) ?? this.createEmptyBundle(bundleKey, protectedContext);
|
|
1109
|
+
nextBundle.guards = guards;
|
|
1110
|
+
nextBundle.ready = true;
|
|
1111
|
+
nextBundle.etag = body.etag ?? "";
|
|
1112
|
+
nextBundle.protectedContext = cloneProtectedContext(protectedContext);
|
|
1113
|
+
const previousRefreshRateSeconds = this.currentRefreshRateSeconds;
|
|
1114
|
+
nextBundle.refreshRateSeconds = positiveNumberOrDefault(
|
|
1115
|
+
body.refreshRateSeconds,
|
|
1116
|
+
this.options.refreshRateSeconds
|
|
1117
|
+
);
|
|
1118
|
+
this.bundles.set(bundleKey, nextBundle);
|
|
1119
|
+
this.recomputeRefreshInterval();
|
|
1120
|
+
if (this.refreshTimer !== null && this.currentRefreshRateSeconds !== previousRefreshRateSeconds) {
|
|
1121
|
+
this.scheduleNextRefresh();
|
|
1122
|
+
}
|
|
1123
|
+
this.emitChange();
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
this.log("[liteguard] Guard fetch error:", error);
|
|
1126
|
+
} finally {
|
|
1127
|
+
cleanup();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** Create an `AbortSignal` that cancels an HTTP request after `timeoutMs`. */
|
|
1131
|
+
createTimeoutSignal(timeoutMs) {
|
|
1132
|
+
const controller = new AbortController();
|
|
1133
|
+
const handle = this.runtime.setTimeout(() => {
|
|
1134
|
+
controller.abort();
|
|
1135
|
+
}, timeoutMs);
|
|
1136
|
+
this.runtime.detachTimer?.(handle);
|
|
1137
|
+
return {
|
|
1138
|
+
signal: controller.signal,
|
|
1139
|
+
cleanup: () => {
|
|
1140
|
+
this.runtime.clearTimeout(handle);
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
/** Send a JSON POST request to the Liteguard backend with the SDK defaults applied. */
|
|
1145
|
+
async post(url, body, keepalive = false) {
|
|
1146
|
+
const { signal, cleanup } = this.createTimeoutSignal(this.options.httpTimeoutSeconds * 1e3);
|
|
1147
|
+
try {
|
|
1148
|
+
const res = await this.runtime.fetch(url.toString(), {
|
|
1149
|
+
method: "POST",
|
|
1150
|
+
headers: {
|
|
1151
|
+
Authorization: `Bearer ${this.projectClientKeyId}`,
|
|
1152
|
+
"Content-Type": "application/json",
|
|
1153
|
+
...this.options.environment && { "X-Liteguard-Environment": this.options.environment }
|
|
1154
|
+
},
|
|
1155
|
+
body,
|
|
1156
|
+
signal,
|
|
1157
|
+
keepalive
|
|
1158
|
+
});
|
|
1159
|
+
if (!res.ok) {
|
|
1160
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
1161
|
+
}
|
|
1162
|
+
} finally {
|
|
1163
|
+
cleanup();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/** Buffer a telemetry signal and trigger an eager flush when the batch is full. */
|
|
1167
|
+
bufferSignal(input) {
|
|
1168
|
+
const metadata = this.nextSignalMetadata(input.parentSignalIdOverride);
|
|
1169
|
+
const signal = {
|
|
1170
|
+
guardName: input.guardName,
|
|
1171
|
+
result: input.result,
|
|
1172
|
+
properties: { ...input.properties },
|
|
1173
|
+
timestampMs: this.runtime.now(),
|
|
1174
|
+
signalId: metadata.signalId,
|
|
1175
|
+
executionId: metadata.executionId,
|
|
1176
|
+
...metadata.parentSignalId ? { parentSignalId: metadata.parentSignalId } : {},
|
|
1177
|
+
sequenceNumber: metadata.sequenceNumber,
|
|
1178
|
+
callsiteId: input.callsiteId,
|
|
1179
|
+
kind: input.kind,
|
|
1180
|
+
droppedSignalsSinceLast: this.takeDroppedSignals(),
|
|
1181
|
+
...input.measurement ? { measurement: input.measurement } : {}
|
|
1182
|
+
};
|
|
1183
|
+
if (this.signalBuffer.length >= this.maxBufferSize()) {
|
|
1184
|
+
this.signalBuffer.shift();
|
|
1185
|
+
this.droppedSignalsPending += 1;
|
|
1186
|
+
}
|
|
1187
|
+
this.signalBuffer.push(signal);
|
|
1188
|
+
if (this.signalBuffer.length >= this.options.flushSize) {
|
|
1189
|
+
void this.flushSignals();
|
|
1190
|
+
}
|
|
1191
|
+
return signal;
|
|
1192
|
+
}
|
|
1193
|
+
/** Allocate signal correlation metadata for the current execution context. */
|
|
1194
|
+
nextSignalMetadata(parentSignalIdOverride) {
|
|
1195
|
+
const state = this.runtime.execution.getStore();
|
|
1196
|
+
const signalId = nextSignalId();
|
|
1197
|
+
if (!state) {
|
|
1198
|
+
return {
|
|
1199
|
+
signalId,
|
|
1200
|
+
executionId: nextSignalId(),
|
|
1201
|
+
sequenceNumber: 1
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
state.sequenceNumber += 1;
|
|
1205
|
+
const parentSignalId = parentSignalIdOverride ?? state.lastSignalId;
|
|
1206
|
+
state.lastSignalId = signalId;
|
|
1207
|
+
return {
|
|
1208
|
+
signalId,
|
|
1209
|
+
executionId: state.executionId,
|
|
1210
|
+
...parentSignalId ? { parentSignalId } : {},
|
|
1211
|
+
sequenceNumber: state.sequenceNumber
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
runWithExplicitExecutionState(state, fn) {
|
|
1215
|
+
const existing = this.runtime.execution.getStore();
|
|
1216
|
+
if (existing === state) {
|
|
1217
|
+
return fn();
|
|
1218
|
+
}
|
|
1219
|
+
return this.runtime.execution.run(state, fn);
|
|
1220
|
+
}
|
|
1221
|
+
/** Capture the first external stack frame so telemetry can identify the call site. */
|
|
1222
|
+
captureCallsiteId() {
|
|
1223
|
+
const err = new Error();
|
|
1224
|
+
const stack = err.stack?.split("\n").slice(1) ?? [];
|
|
1225
|
+
const frame = stack.find((line) => {
|
|
1226
|
+
if (line.includes("node:internal")) return false;
|
|
1227
|
+
return !this.runtime.internalStackMarkers.some((marker) => line.includes(marker));
|
|
1228
|
+
});
|
|
1229
|
+
return frame?.trim().replace(/^at\s+/, "") || "unknown";
|
|
1230
|
+
}
|
|
1231
|
+
/** Build the in-memory rate-limit bucket key for a guard and property set. */
|
|
1232
|
+
rateLimitBucketKey(name, rateLimitProperties, props) {
|
|
1233
|
+
if (rateLimitProperties.length === 0) return name;
|
|
1234
|
+
const parts = rateLimitProperties.map((property) => `${property}=${String(props[property] ?? "")}`);
|
|
1235
|
+
return `${name}\0${parts.join("\0")}`;
|
|
1236
|
+
}
|
|
1237
|
+
checkRateLimit(name, limitPerMinute, rateLimitProperties, props) {
|
|
1238
|
+
const now = this.runtime.monotonicNow();
|
|
1239
|
+
const key = this.rateLimitBucketKey(name, rateLimitProperties, props);
|
|
1240
|
+
const state = this.rateLimitState.get(key) ?? { windowStart: now, count: 0 };
|
|
1241
|
+
if (now - state.windowStart >= 6e4) {
|
|
1242
|
+
state.windowStart = now;
|
|
1243
|
+
state.count = 0;
|
|
1244
|
+
}
|
|
1245
|
+
if (state.count >= limitPerMinute) {
|
|
1246
|
+
this.rateLimitState.set(key, state);
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
state.count += 1;
|
|
1250
|
+
this.rateLimitState.set(key, state);
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
/** Check whether a guard would pass the current rate-limit window without consuming a slot. */
|
|
1254
|
+
wouldPassRateLimit(name, limitPerMinute, rateLimitProperties, props) {
|
|
1255
|
+
const now = this.runtime.monotonicNow();
|
|
1256
|
+
const key = this.rateLimitBucketKey(name, rateLimitProperties, props);
|
|
1257
|
+
const state = this.rateLimitState.get(key);
|
|
1258
|
+
if (!state) {
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
if (now - state.windowStart >= 6e4) {
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
return state.count < limitPerMinute;
|
|
1265
|
+
}
|
|
1266
|
+
/** Decide whether performance measurement should be captured for this guard call. */
|
|
1267
|
+
isMeasurementEnabled(guard, options) {
|
|
1268
|
+
if (this.options.disableMeasurement) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
if (options.disableMeasurement) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
if (guard.disableMeasurement === true) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
/** Return the hard cap for the in-memory signal buffer after flush failures. */
|
|
1280
|
+
maxBufferSize() {
|
|
1281
|
+
return this.options.flushSize * this.options.flushBufferMultiplier;
|
|
1282
|
+
}
|
|
1283
|
+
/** Drain and reset the count of dropped signals to attach to the next emitted signal. */
|
|
1284
|
+
takeDroppedSignals() {
|
|
1285
|
+
const dropped = this.droppedSignalsPending;
|
|
1286
|
+
this.droppedSignalsPending = 0;
|
|
1287
|
+
return dropped;
|
|
1288
|
+
}
|
|
1289
|
+
/** Write a warning only when quiet mode is disabled. */
|
|
1290
|
+
log(...args) {
|
|
1291
|
+
if (!this.options.quiet) {
|
|
1292
|
+
console.warn(...args);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/** @internal Test helper for swapping the public guard bundle in memory. */
|
|
1296
|
+
_setGuards(guards) {
|
|
1297
|
+
const bundle = this.getBundle(PUBLIC_BUNDLE_KEY) ?? this.createEmptyBundle(PUBLIC_BUNDLE_KEY, null);
|
|
1298
|
+
bundle.guards = new Map(guards.map((guard) => [guard.name, guard]));
|
|
1299
|
+
bundle.ready = true;
|
|
1300
|
+
bundle.etag = "";
|
|
1301
|
+
this.bundles.set(PUBLIC_BUNDLE_KEY, bundle);
|
|
1302
|
+
this.emitChange();
|
|
1303
|
+
}
|
|
1304
|
+
/** @internal Test helper for swapping a protected-context bundle in memory. */
|
|
1305
|
+
_setProtectedGuards(protectedContext, guards) {
|
|
1306
|
+
const key = protectedContextCacheKey(protectedContext);
|
|
1307
|
+
const bundle = this.getBundle(key) ?? this.createEmptyBundle(key, protectedContext);
|
|
1308
|
+
bundle.guards = new Map(guards.map((guard) => [guard.name, guard]));
|
|
1309
|
+
bundle.ready = true;
|
|
1310
|
+
bundle.etag = "";
|
|
1311
|
+
bundle.protectedContext = cloneProtectedContext(protectedContext);
|
|
1312
|
+
this.bundles.set(key, bundle);
|
|
1313
|
+
this.recomputeRefreshInterval();
|
|
1314
|
+
this.emitChange();
|
|
1315
|
+
}
|
|
1316
|
+
/** @internal Clear all rate-limit counters, or only those for a specific guard. */
|
|
1317
|
+
_resetRateLimitState(name) {
|
|
1318
|
+
if (name !== void 0) {
|
|
1319
|
+
this.rateLimitState.delete(name);
|
|
1320
|
+
const prefix = name + "\0";
|
|
1321
|
+
for (const key of this.rateLimitState.keys()) {
|
|
1322
|
+
if (key.startsWith(prefix)) {
|
|
1323
|
+
this.rateLimitState.delete(key);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this.rateLimitState.clear();
|
|
1329
|
+
}
|
|
1330
|
+
/** @internal Return the default scope properties for tests and diagnostics. */
|
|
1331
|
+
_getContext() {
|
|
1332
|
+
return this.defaultScope.getProperties();
|
|
1333
|
+
}
|
|
1334
|
+
/** @internal Return the default scope protected context for tests and diagnostics. */
|
|
1335
|
+
_getProtectedContext() {
|
|
1336
|
+
return this.defaultScope.getProtectedContext();
|
|
1337
|
+
}
|
|
1338
|
+
/** @internal Return the active refresh cadence for tests and diagnostics. */
|
|
1339
|
+
_getRefreshRateSeconds() {
|
|
1340
|
+
return this.currentRefreshRateSeconds;
|
|
1341
|
+
}
|
|
1342
|
+
/** @internal Return a cloned view of the buffered signals for tests and diagnostics. */
|
|
1343
|
+
_getSignalBuffer() {
|
|
1344
|
+
return this.signalBuffer.map((signal) => ({
|
|
1345
|
+
...signal,
|
|
1346
|
+
...signal.properties ? { properties: { ...signal.properties } } : {}
|
|
1347
|
+
}));
|
|
1348
|
+
}
|
|
1349
|
+
/** @internal Return the sorted set of queued unadopted guards. */
|
|
1350
|
+
_getPendingUnadoptedGuards() {
|
|
1351
|
+
return [...this.pendingUnadoptedGuards].sort();
|
|
1352
|
+
}
|
|
1353
|
+
/** @internal Return the currently cached bundle keys. */
|
|
1354
|
+
_getBundleKeys() {
|
|
1355
|
+
return [...this.bundles.keys()].sort();
|
|
1356
|
+
}
|
|
1357
|
+
/** @internal Expose the request-scope store for tests. */
|
|
1358
|
+
_getRequestScopeStore() {
|
|
1359
|
+
return this.runtime.requestScope?.getStore();
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
function nextSignalId() {
|
|
1363
|
+
signalCounter += 1;
|
|
1364
|
+
return `${Date.now().toString(36)}-${signalCounter.toString(36)}`;
|
|
1365
|
+
}
|
|
1366
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1367
|
+
0 && (module.exports = {
|
|
1368
|
+
BaseLiteguardClient,
|
|
1369
|
+
LiteguardScope,
|
|
1370
|
+
evaluateGuard
|
|
1371
|
+
});
|
|
1372
|
+
//# sourceMappingURL=index.js.map
|