New:Socket for Asana Is Now Available.Learn more
Get Started

@archstone/emitter-support

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@archstone/emitter-support - npm Package Compare versions

Comparing version
0.11.7
to
0.12.0
+74
-1
dist/index.d.ts

@@ -298,2 +298,75 @@ import { Lifecycle, IRTool, IR, IRField, IRResourceRegistry } from '@archstone/compiler';

}
/**
* The store a distributed `SharedWindowRateLimitCounter` writes through. Deliberately ONE
* method, and deliberately the exact shape Redis's `INCR` + `EXPIRE` pair already has, because
* that is the shape almost every shared store can honour: **atomically increment the integer at
* `key`, ensure it expires in at most `ttlSeconds`, and return the value after the increment.**
*
* The atomicity requirement is not decorative. Two instances incrementing the same key
* concurrently must observe two different return values, or the limit silently becomes
* "N per window per instance" — which is exactly the defect `InMemoryRateLimitCounter` has by
* construction, moved to a shared store and made harder to see. A read-modify-write over an
* eventually-consistent KV (Cloudflare KV, S3) does **not** satisfy this; a Redis-compatible
* store, a Cloudflare Durable Object, or a single SQL row updated with `RETURNING` does.
*/
interface SharedCounterStore {
incrementWithTtl(key: string, ttlSeconds: number): Promise<number>;
}
/**
* A `RateLimitCounter` for multi-instance and edge deployments — the production counterpart to
* `InMemoryRateLimitCounter`, which is per-process and therefore not a limit at all once more
* than one process serves traffic.
*
* Windowing is **fixed**, identical to `InMemoryRateLimitCounter`'s and for the same reason
* (the interface leaves it to the implementation, and two shipped implementations that disagree
* about what a window is would be a trap): time is sliced into non-overlapping
* `windowSeconds`-wide buckets aligned to the epoch, and the bucket start is folded into the
* store key. The counter therefore never has to read, compare or reset anything — a new window
* is simply a new key, which is why a store only needs `INCR`-with-TTL and never a transaction.
*
* TTL is `windowSeconds` plus a small grace, so a key outlives its own window slightly rather
* than expiring underneath a request that is still being counted, and no key survives longer
* than it can be useful. Nothing depends on the grace value for correctness — a key that
* expires early can only ever undercount toward zero, never over the limit.
*
* **Clock skew across instances is real and bounded here.** Two instances whose clocks differ
* by less than one window agree on the bucket for all but the instants near a boundary; at the
* boundary a request may land in the neighbouring bucket. That is the standard fixed-window
* trade and it is stated rather than hidden: a limit of N per window admits up to 2N across an
* unlucky boundary pair. Deployments that cannot accept that need a sliding-window store
* implementation, which this interface permits (the store may key however it likes) but this
* class does not attempt.
*/
declare class SharedWindowRateLimitCounter implements RateLimitCounter {
private readonly store;
private readonly now;
private readonly prefix;
private readonly graceSeconds;
constructor(store: SharedCounterStore, opts?: {
/** Injectable for deterministic tests, exactly as `InMemoryRateLimitCounter` does. */
now?: () => number;
/** Namespace for the store keys, so one Redis/DO can serve several deployments. */
prefix?: string;
/** Extra seconds of key lifetime beyond the window. Correctness does not depend on it. */
graceSeconds?: number;
});
increment(key: string, windowSeconds: number): Promise<number>;
}
/**
* Adapter for any Redis-compatible client — `ioredis`, `node-redis`, Upstash's REST client —
* **duck-typed on purpose**: this package takes no dependency on any of them, and the deployer
* passes the client they already have. The two methods used are the two every one of them
* exposes.
*
* `incr` then `expire` is two round-trips and is NOT a transaction. That is safe here for one
* specific reason worth stating: `incr` alone is the atomic part that decides the returned
* count, and `expire` only bounds the key's lifetime. A crash between them leaves a key with no
* TTL — it keeps counting for that window and is superseded by the next window's key, so the
* failure mode is a leaked key, never a missed limit. Deployers who mind the leak can pass a
* client whose `incr` is a Lua script or pipeline instead; the interface does not care.
*/
declare function redisSharedCounterStore(client: {
incr(key: string): Promise<number>;
expire(key: string, seconds: number): Promise<unknown>;
}): SharedCounterStore;
/** #45 / ADD-45 D-2: the one new reason code this increment adds. Distinct from every code

@@ -596,2 +669,2 @@ * `evaluatePolicy` returns (BR-29's closed four) and from the two lifecycle codes — a client or

export { type AuditCaller, type AuditSink, type AuditWritable, type BuildExecutionRecordInput, type ExecutionConsumer, type ExecutionDenialReason, type ExecutionPhase, type ExecutionRecord, type ExecutionStatus, type Exposure, type ExposureHint, type HealthStatus, type HintLevel, InMemoryRateLimitCounter, LIFECYCLE_BLOCKED_REASON, LIFECYCLE_UNEVALUATABLE_REASON, type MappingResult, type MappingStatus, type NamedTool, type PolicyCaller, type PolicyDecision, type PolicyDenial, type PolicyDenialReason, RATE_LIMIT_EXCEEDED_REASON, REDACTED, type RateLimitCounter, type RateLimitDecision, type RateLimitDenial, type RateLimitDenialReason, Registry, type ToolNameCollision, applyResponseMapping, auditNow, buildExecutionRecord, combineExposure, contractViolationMessage, emitExecutionRecord, evaluatePolicy, evaluateRateLimit, inputJsonSchema, jsonLinesAuditSink, lifecycleExposure, objectJsonSchema, toolName };
export { type AuditCaller, type AuditSink, type AuditWritable, type BuildExecutionRecordInput, type ExecutionConsumer, type ExecutionDenialReason, type ExecutionPhase, type ExecutionRecord, type ExecutionStatus, type Exposure, type ExposureHint, type HealthStatus, type HintLevel, InMemoryRateLimitCounter, LIFECYCLE_BLOCKED_REASON, LIFECYCLE_UNEVALUATABLE_REASON, type MappingResult, type MappingStatus, type NamedTool, type PolicyCaller, type PolicyDecision, type PolicyDenial, type PolicyDenialReason, RATE_LIMIT_EXCEEDED_REASON, REDACTED, type RateLimitCounter, type RateLimitDecision, type RateLimitDenial, type RateLimitDenialReason, Registry, type SharedCounterStore, SharedWindowRateLimitCounter, type ToolNameCollision, applyResponseMapping, auditNow, buildExecutionRecord, combineExposure, contractViolationMessage, emitExecutionRecord, evaluatePolicy, evaluateRateLimit, inputJsonSchema, jsonLinesAuditSink, lifecycleExposure, objectJsonSchema, redisSharedCounterStore, toolName };

@@ -360,2 +360,31 @@ // src/lowering.ts

};
var SharedWindowRateLimitCounter = class {
store;
now;
prefix;
graceSeconds;
constructor(store, opts = {}) {
this.store = store;
this.now = opts.now ?? (() => Date.now());
this.prefix = opts.prefix ?? "archstone:rl";
this.graceSeconds = opts.graceSeconds ?? 5;
}
async increment(key, windowSeconds) {
const windowMs = windowSeconds * 1e3;
const windowStart = Math.floor(this.now() / windowMs) * windowMs;
return this.store.incrementWithTtl(
`${this.prefix}:${key}:${windowStart}`,
windowSeconds + this.graceSeconds
);
}
};
function redisSharedCounterStore(client) {
return {
async incrementWithTtl(key, ttlSeconds) {
const count = await client.incr(key);
if (count === 1) await client.expire(key, ttlSeconds);
return count;
}
};
}
var RATE_LIMIT_EXCEEDED_REASON = "rate_limit_exceeded";

@@ -378,5 +407,13 @@ var ALLOWED2 = { allowed: true };

}
const counts = await Promise.all(
rules.map((rule) => counter.increment(rateLimitKey(rule.id, caller.principal), rule.rateLimit.windowSeconds))
);
let counts;
try {
counts = await Promise.all(
rules.map((rule) => counter.increment(rateLimitKey(rule.id, caller.principal), rule.rateLimit.windowSeconds))
);
} catch {
return deny2(
"policy_unevaluatable",
`capability '${tool.id}' declares spec.rateLimit but its RateLimitCounter could not be consulted \u2014 refusing (fail-closed)`
);
}
for (let i = 0; i < rules.length; i++) {

@@ -511,2 +548,3 @@ const { maxInvocations, windowSeconds } = rules[i].rateLimit;

Registry,
SharedWindowRateLimitCounter,
applyResponseMapping,

@@ -524,4 +562,5 @@ auditNow,

objectJsonSchema,
redisSharedCounterStore,
toolName
};
//# sourceMappingURL=index.js.map
+3
-3
{
"name": "@archstone/emitter-support",
"version": "0.11.7",
"version": "0.12.0",
"private": false,

@@ -40,7 +40,7 @@ "type": "module",

"dependencies": {
"@archstone/compiler": "0.11.7"
"@archstone/compiler": "0.12.0"
},
"devDependencies": {
"tsup": "^8.5.1",
"@archstone/schema": "0.11.7"
"@archstone/schema": "0.12.0"
},

@@ -47,0 +47,0 @@ "scripts": {

Sorry, the diff of this file is too big to display