@saas-maker/foundry-shield 0.1.0
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.d.mts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +133 -0
- package/dist/index.mjs +102 -0
- package/package.json +16 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface RateLimitStore {
|
|
2
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
3
|
+
reset(key: string): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
declare class MemoryStore implements RateLimitStore {
|
|
6
|
+
private windows;
|
|
7
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
8
|
+
reset(key: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ShieldConfig {
|
|
12
|
+
store: RateLimitStore;
|
|
13
|
+
windowMs?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
keyPrefix?: string;
|
|
16
|
+
project?: string;
|
|
17
|
+
}
|
|
18
|
+
interface ShieldResult {
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
count: number;
|
|
21
|
+
remaining: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
}
|
|
24
|
+
declare class Shield {
|
|
25
|
+
private store;
|
|
26
|
+
private windowMs;
|
|
27
|
+
private max;
|
|
28
|
+
private keyPrefix;
|
|
29
|
+
constructor(config: ShieldConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Check if a key is within rate limit.
|
|
32
|
+
* Returns result — caller decides whether to block.
|
|
33
|
+
*/
|
|
34
|
+
check(identifier: string): Promise<ShieldResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Assert rate limit — throws FoundryError.rateLimit() if exceeded.
|
|
37
|
+
*/
|
|
38
|
+
assert(identifier: string): Promise<void>;
|
|
39
|
+
reset(identifier: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Convenience: create a Shield with memory store */
|
|
42
|
+
declare function memoryShield(config?: Partial<ShieldConfig>): Shield;
|
|
43
|
+
/** Convenience: create a Shield with D1 store */
|
|
44
|
+
declare function d1Shield(d1: any, config?: Partial<ShieldConfig>): Shield;
|
|
45
|
+
|
|
46
|
+
declare class D1Store implements RateLimitStore {
|
|
47
|
+
private d1;
|
|
48
|
+
constructor(d1: {
|
|
49
|
+
prepare: (sql: string) => any;
|
|
50
|
+
});
|
|
51
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
52
|
+
reset(key: string): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { D1Store, MemoryStore, type RateLimitStore, Shield, type ShieldConfig, type ShieldResult, d1Shield, memoryShield };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface RateLimitStore {
|
|
2
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
3
|
+
reset(key: string): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
declare class MemoryStore implements RateLimitStore {
|
|
6
|
+
private windows;
|
|
7
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
8
|
+
reset(key: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ShieldConfig {
|
|
12
|
+
store: RateLimitStore;
|
|
13
|
+
windowMs?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
keyPrefix?: string;
|
|
16
|
+
project?: string;
|
|
17
|
+
}
|
|
18
|
+
interface ShieldResult {
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
count: number;
|
|
21
|
+
remaining: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
}
|
|
24
|
+
declare class Shield {
|
|
25
|
+
private store;
|
|
26
|
+
private windowMs;
|
|
27
|
+
private max;
|
|
28
|
+
private keyPrefix;
|
|
29
|
+
constructor(config: ShieldConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Check if a key is within rate limit.
|
|
32
|
+
* Returns result — caller decides whether to block.
|
|
33
|
+
*/
|
|
34
|
+
check(identifier: string): Promise<ShieldResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Assert rate limit — throws FoundryError.rateLimit() if exceeded.
|
|
37
|
+
*/
|
|
38
|
+
assert(identifier: string): Promise<void>;
|
|
39
|
+
reset(identifier: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/** Convenience: create a Shield with memory store */
|
|
42
|
+
declare function memoryShield(config?: Partial<ShieldConfig>): Shield;
|
|
43
|
+
/** Convenience: create a Shield with D1 store */
|
|
44
|
+
declare function d1Shield(d1: any, config?: Partial<ShieldConfig>): Shield;
|
|
45
|
+
|
|
46
|
+
declare class D1Store implements RateLimitStore {
|
|
47
|
+
private d1;
|
|
48
|
+
constructor(d1: {
|
|
49
|
+
prepare: (sql: string) => any;
|
|
50
|
+
});
|
|
51
|
+
increment(key: string, windowMs: number): Promise<number>;
|
|
52
|
+
reset(key: string): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { D1Store, MemoryStore, type RateLimitStore, Shield, type ShieldConfig, type ShieldResult, d1Shield, memoryShield };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
D1Store: () => D1Store,
|
|
24
|
+
MemoryStore: () => MemoryStore,
|
|
25
|
+
Shield: () => Shield,
|
|
26
|
+
d1Shield: () => d1Shield,
|
|
27
|
+
memoryShield: () => memoryShield
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/limiter.ts
|
|
32
|
+
var import_ops = require("@saas-maker/ops");
|
|
33
|
+
|
|
34
|
+
// src/stores/memory.ts
|
|
35
|
+
var MemoryStore = class {
|
|
36
|
+
windows = /* @__PURE__ */ new Map();
|
|
37
|
+
async increment(key, windowMs) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const existing = this.windows.get(key);
|
|
40
|
+
if (!existing || now >= existing.resetAt) {
|
|
41
|
+
this.windows.set(key, { count: 1, resetAt: now + windowMs });
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
existing.count++;
|
|
45
|
+
return existing.count;
|
|
46
|
+
}
|
|
47
|
+
async reset(key) {
|
|
48
|
+
this.windows.delete(key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/stores/d1.ts
|
|
53
|
+
var D1Store = class {
|
|
54
|
+
constructor(d1) {
|
|
55
|
+
this.d1 = d1;
|
|
56
|
+
}
|
|
57
|
+
d1;
|
|
58
|
+
async increment(key, windowMs) {
|
|
59
|
+
const windowSec = Math.ceil(windowMs / 1e3);
|
|
60
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
61
|
+
const windowStart = now - windowSec;
|
|
62
|
+
await this.d1.prepare(
|
|
63
|
+
`DELETE FROM shield_requests WHERE key = ? AND ts < ?`
|
|
64
|
+
).bind(key, windowStart).run();
|
|
65
|
+
await this.d1.prepare(
|
|
66
|
+
`INSERT INTO shield_requests (key, ts) VALUES (?, ?)`
|
|
67
|
+
).bind(key, now).run();
|
|
68
|
+
const row = await this.d1.prepare(
|
|
69
|
+
`SELECT COUNT(*) as count FROM shield_requests WHERE key = ? AND ts >= ?`
|
|
70
|
+
).bind(key, windowStart).first();
|
|
71
|
+
return row?.count ?? 1;
|
|
72
|
+
}
|
|
73
|
+
async reset(key) {
|
|
74
|
+
await this.d1.prepare(`DELETE FROM shield_requests WHERE key = ?`).bind(key).run();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/limiter.ts
|
|
79
|
+
var Shield = class {
|
|
80
|
+
store;
|
|
81
|
+
windowMs;
|
|
82
|
+
max;
|
|
83
|
+
keyPrefix;
|
|
84
|
+
constructor(config) {
|
|
85
|
+
this.store = config.store;
|
|
86
|
+
this.windowMs = config.windowMs ?? 6e4;
|
|
87
|
+
this.max = config.max ?? 60;
|
|
88
|
+
this.keyPrefix = config.keyPrefix ?? "shield";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a key is within rate limit.
|
|
92
|
+
* Returns result — caller decides whether to block.
|
|
93
|
+
*/
|
|
94
|
+
async check(identifier) {
|
|
95
|
+
const key = `${this.keyPrefix}:${identifier}`;
|
|
96
|
+
const count = await this.store.increment(key, this.windowMs);
|
|
97
|
+
return {
|
|
98
|
+
allowed: count <= this.max,
|
|
99
|
+
count,
|
|
100
|
+
remaining: Math.max(0, this.max - count),
|
|
101
|
+
limit: this.max
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Assert rate limit — throws FoundryError.rateLimit() if exceeded.
|
|
106
|
+
*/
|
|
107
|
+
async assert(identifier) {
|
|
108
|
+
const result = await this.check(identifier);
|
|
109
|
+
if (!result.allowed) {
|
|
110
|
+
throw import_ops.FoundryErrors.rateLimit(
|
|
111
|
+
`Rate limit exceeded: ${result.count}/${result.limit} requests in window`,
|
|
112
|
+
{ identifier, count: result.count, limit: result.limit }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async reset(identifier) {
|
|
117
|
+
await this.store.reset(`${this.keyPrefix}:${identifier}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function memoryShield(config) {
|
|
121
|
+
return new Shield({ store: new MemoryStore(), ...config });
|
|
122
|
+
}
|
|
123
|
+
function d1Shield(d1, config) {
|
|
124
|
+
return new Shield({ store: new D1Store(d1), ...config });
|
|
125
|
+
}
|
|
126
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
127
|
+
0 && (module.exports = {
|
|
128
|
+
D1Store,
|
|
129
|
+
MemoryStore,
|
|
130
|
+
Shield,
|
|
131
|
+
d1Shield,
|
|
132
|
+
memoryShield
|
|
133
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/limiter.ts
|
|
2
|
+
import { FoundryErrors } from "@saas-maker/ops";
|
|
3
|
+
|
|
4
|
+
// src/stores/memory.ts
|
|
5
|
+
var MemoryStore = class {
|
|
6
|
+
windows = /* @__PURE__ */ new Map();
|
|
7
|
+
async increment(key, windowMs) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const existing = this.windows.get(key);
|
|
10
|
+
if (!existing || now >= existing.resetAt) {
|
|
11
|
+
this.windows.set(key, { count: 1, resetAt: now + windowMs });
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
existing.count++;
|
|
15
|
+
return existing.count;
|
|
16
|
+
}
|
|
17
|
+
async reset(key) {
|
|
18
|
+
this.windows.delete(key);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/stores/d1.ts
|
|
23
|
+
var D1Store = class {
|
|
24
|
+
constructor(d1) {
|
|
25
|
+
this.d1 = d1;
|
|
26
|
+
}
|
|
27
|
+
d1;
|
|
28
|
+
async increment(key, windowMs) {
|
|
29
|
+
const windowSec = Math.ceil(windowMs / 1e3);
|
|
30
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
31
|
+
const windowStart = now - windowSec;
|
|
32
|
+
await this.d1.prepare(
|
|
33
|
+
`DELETE FROM shield_requests WHERE key = ? AND ts < ?`
|
|
34
|
+
).bind(key, windowStart).run();
|
|
35
|
+
await this.d1.prepare(
|
|
36
|
+
`INSERT INTO shield_requests (key, ts) VALUES (?, ?)`
|
|
37
|
+
).bind(key, now).run();
|
|
38
|
+
const row = await this.d1.prepare(
|
|
39
|
+
`SELECT COUNT(*) as count FROM shield_requests WHERE key = ? AND ts >= ?`
|
|
40
|
+
).bind(key, windowStart).first();
|
|
41
|
+
return row?.count ?? 1;
|
|
42
|
+
}
|
|
43
|
+
async reset(key) {
|
|
44
|
+
await this.d1.prepare(`DELETE FROM shield_requests WHERE key = ?`).bind(key).run();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/limiter.ts
|
|
49
|
+
var Shield = class {
|
|
50
|
+
store;
|
|
51
|
+
windowMs;
|
|
52
|
+
max;
|
|
53
|
+
keyPrefix;
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.store = config.store;
|
|
56
|
+
this.windowMs = config.windowMs ?? 6e4;
|
|
57
|
+
this.max = config.max ?? 60;
|
|
58
|
+
this.keyPrefix = config.keyPrefix ?? "shield";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a key is within rate limit.
|
|
62
|
+
* Returns result — caller decides whether to block.
|
|
63
|
+
*/
|
|
64
|
+
async check(identifier) {
|
|
65
|
+
const key = `${this.keyPrefix}:${identifier}`;
|
|
66
|
+
const count = await this.store.increment(key, this.windowMs);
|
|
67
|
+
return {
|
|
68
|
+
allowed: count <= this.max,
|
|
69
|
+
count,
|
|
70
|
+
remaining: Math.max(0, this.max - count),
|
|
71
|
+
limit: this.max
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Assert rate limit — throws FoundryError.rateLimit() if exceeded.
|
|
76
|
+
*/
|
|
77
|
+
async assert(identifier) {
|
|
78
|
+
const result = await this.check(identifier);
|
|
79
|
+
if (!result.allowed) {
|
|
80
|
+
throw FoundryErrors.rateLimit(
|
|
81
|
+
`Rate limit exceeded: ${result.count}/${result.limit} requests in window`,
|
|
82
|
+
{ identifier, count: result.count, limit: result.limit }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async reset(identifier) {
|
|
87
|
+
await this.store.reset(`${this.keyPrefix}:${identifier}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function memoryShield(config) {
|
|
91
|
+
return new Shield({ store: new MemoryStore(), ...config });
|
|
92
|
+
}
|
|
93
|
+
function d1Shield(d1, config) {
|
|
94
|
+
return new Shield({ store: new D1Store(d1), ...config });
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
D1Store,
|
|
98
|
+
MemoryStore,
|
|
99
|
+
Shield,
|
|
100
|
+
d1Shield,
|
|
101
|
+
memoryShield
|
|
102
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saas-maker/foundry-shield",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Foundry rate limiting — sliding window with D1/memory backends",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "import": "./dist/index.mjs", "require": "./dist/index.js", "types": "./dist/index.d.ts" }
|
|
10
|
+
},
|
|
11
|
+
"files": ["dist"],
|
|
12
|
+
"scripts": { "build": "tsup src/index.ts --format esm,cjs --dts" },
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"dependencies": { "@saas-maker/ops": "^0.1.0" },
|
|
15
|
+
"devDependencies": { "tsup": "^8.0.0", "typescript": "^5.9.0" }
|
|
16
|
+
}
|