keypointjs 1.0.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/LICENSE +201 -0
- package/README.md +808 -0
- package/package.json +72 -0
- package/src/core/Context.js +104 -0
- package/src/core/ProtocolEngine.js +144 -0
- package/src/keypoint/Keypoint.js +36 -0
- package/src/keypoint/KeypointContext.js +88 -0
- package/src/keypoint/KeypointStorage.js +236 -0
- package/src/keypoint/KeypointValidator.js +51 -0
- package/src/keypoint/ScopeManager.js +206 -0
- package/src/keypointJS.js +779 -0
- package/src/plugins/AuditLogger.js +294 -0
- package/src/plugins/PluginManager.js +303 -0
- package/src/plugins/RateLimiter.js +24 -0
- package/src/plugins/WebSocketGuard.js +351 -0
- package/src/policy/AccessDecision.js +104 -0
- package/src/policy/PolicyEngine.js +82 -0
- package/src/policy/PolicyRule.js +246 -0
- package/src/router/MinimalRouter.js +41 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export class PolicyRule {
|
|
2
|
+
constructor(name, evaluator, options = {}) {
|
|
3
|
+
this.name = name;
|
|
4
|
+
this.evaluator = evaluator;
|
|
5
|
+
this.options = {
|
|
6
|
+
priority: 0,
|
|
7
|
+
enabled: true,
|
|
8
|
+
description: '',
|
|
9
|
+
...options
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async evaluate(context) {
|
|
14
|
+
if (!this.options.enabled) {
|
|
15
|
+
return { allowed: true, rule: this.name };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await this.evaluator(context);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
allowed: result.allowed !== false,
|
|
23
|
+
reason: result.reason || '',
|
|
24
|
+
metadata: result.metadata || {},
|
|
25
|
+
rule: this.name,
|
|
26
|
+
timestamp: new Date()
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
allowed: false,
|
|
31
|
+
reason: `Rule evaluation failed: ${error.message}`,
|
|
32
|
+
metadata: { error: error.message },
|
|
33
|
+
rule: this.name,
|
|
34
|
+
timestamp: new Date()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enable() {
|
|
40
|
+
this.options.enabled = true;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
disable() {
|
|
45
|
+
this.options.enabled = false;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setPriority(priority) {
|
|
50
|
+
this.options.priority = priority;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Built-in policy rules
|
|
56
|
+
export class BuiltInRules {
|
|
57
|
+
static methodRule(allowedMethods = ['GET', 'POST']) {
|
|
58
|
+
return new PolicyRule(
|
|
59
|
+
'method_check',
|
|
60
|
+
async (ctx) => {
|
|
61
|
+
if (!allowedMethods.includes(ctx.method.toUpperCase())) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: false,
|
|
64
|
+
reason: `Method ${ctx.method} not allowed. Allowed: ${allowedMethods.join(', ')}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { allowed: true };
|
|
68
|
+
},
|
|
69
|
+
{ description: 'Check HTTP method' }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static originRule(allowedOrigins = []) {
|
|
74
|
+
return new PolicyRule(
|
|
75
|
+
'origin_check',
|
|
76
|
+
async (ctx) => {
|
|
77
|
+
const origin = ctx.getHeader('origin');
|
|
78
|
+
if (!origin) return { allowed: true };
|
|
79
|
+
|
|
80
|
+
if (allowedOrigins.length === 0) return { allowed: true };
|
|
81
|
+
if (allowedOrigins.includes('*')) return { allowed: true };
|
|
82
|
+
|
|
83
|
+
if (!allowedOrigins.includes(origin)) {
|
|
84
|
+
return {
|
|
85
|
+
allowed: false,
|
|
86
|
+
reason: `Origin ${origin} not allowed`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { allowed: true };
|
|
90
|
+
},
|
|
91
|
+
{ description: 'Check request origin' }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static ipRule(allowedIPs = [], blockedIPs = []) {
|
|
96
|
+
return new PolicyRule(
|
|
97
|
+
'ip_check',
|
|
98
|
+
async (ctx) => {
|
|
99
|
+
const ip = ctx.ip;
|
|
100
|
+
|
|
101
|
+
// Check blocked first
|
|
102
|
+
if (blockedIPs.includes(ip) || this.isIPInRange(ip, blockedIPs)) {
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
reason: `IP ${ip} is blocked`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check allowed (if specified)
|
|
110
|
+
if (allowedIPs.length > 0) {
|
|
111
|
+
if (!allowedIPs.includes(ip) && !this.isIPInRange(ip, allowedIPs)) {
|
|
112
|
+
return {
|
|
113
|
+
allowed: false,
|
|
114
|
+
reason: `IP ${ip} not allowed`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { allowed: true };
|
|
120
|
+
},
|
|
121
|
+
{ description: 'Check IP address' }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static timeWindowRule(startHour = 0, endHour = 24) {
|
|
126
|
+
return new PolicyRule(
|
|
127
|
+
'time_window',
|
|
128
|
+
async (ctx) => {
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const hour = now.getHours();
|
|
131
|
+
|
|
132
|
+
if (hour < startHour || hour >= endHour) {
|
|
133
|
+
return {
|
|
134
|
+
allowed: false,
|
|
135
|
+
reason: `Access only allowed between ${startHour}:00 and ${endHour}:00`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return { allowed: true };
|
|
139
|
+
},
|
|
140
|
+
{ description: 'Check time window' }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static rateLimitRule(limit = 100, windowSeconds = 60) {
|
|
145
|
+
const requests = new Map();
|
|
146
|
+
|
|
147
|
+
return new PolicyRule(
|
|
148
|
+
'rate_limit',
|
|
149
|
+
async (ctx) => {
|
|
150
|
+
const key = ctx.getKeypointId() || ctx.ip;
|
|
151
|
+
const now = Math.floor(Date.now() / 1000);
|
|
152
|
+
const windowStart = now - windowSeconds;
|
|
153
|
+
|
|
154
|
+
// Clean old requests
|
|
155
|
+
const entry = requests.get(key) || { count: 0, timestamps: [] };
|
|
156
|
+
entry.timestamps = entry.timestamps.filter(t => t > windowStart);
|
|
157
|
+
|
|
158
|
+
// Check limit
|
|
159
|
+
if (entry.timestamps.length >= limit) {
|
|
160
|
+
return {
|
|
161
|
+
allowed: false,
|
|
162
|
+
reason: 'Rate limit exceeded',
|
|
163
|
+
metadata: {
|
|
164
|
+
limit,
|
|
165
|
+
remaining: 0,
|
|
166
|
+
reset: entry.timestamps[0] + windowSeconds
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add current request
|
|
172
|
+
entry.timestamps.push(now);
|
|
173
|
+
entry.count = entry.timestamps.length;
|
|
174
|
+
requests.set(key, entry);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
allowed: true,
|
|
178
|
+
metadata: {
|
|
179
|
+
limit,
|
|
180
|
+
remaining: limit - entry.count,
|
|
181
|
+
reset: now + windowSeconds
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
{ description: 'Rate limiting' }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static scopeRule(requiredScope) {
|
|
190
|
+
return new PolicyRule(
|
|
191
|
+
'scope_check',
|
|
192
|
+
async (ctx) => {
|
|
193
|
+
if (!ctx.hasScope(requiredScope)) {
|
|
194
|
+
return {
|
|
195
|
+
allowed: false,
|
|
196
|
+
reason: `Required scope: ${requiredScope}`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { allowed: true };
|
|
200
|
+
},
|
|
201
|
+
{ description: 'Check keypoint scope' }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static protocolRule(allowedProtocols = ['https']) {
|
|
206
|
+
return new PolicyRule(
|
|
207
|
+
'protocol_check',
|
|
208
|
+
async (ctx) => {
|
|
209
|
+
if (!allowedProtocols.includes(ctx.protocol)) {
|
|
210
|
+
return {
|
|
211
|
+
allowed: false,
|
|
212
|
+
reason: `Protocol ${ctx.protocol} not allowed. Allowed: ${allowedProtocols.join(', ')}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return { allowed: true };
|
|
216
|
+
},
|
|
217
|
+
{ description: 'Check protocol' }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static isIPInRange(ip, ranges) {
|
|
222
|
+
for (const range of ranges) {
|
|
223
|
+
if (range.includes('/')) {
|
|
224
|
+
// CIDR notation
|
|
225
|
+
if (this.isIPInCIDR(ip, range)) return true;
|
|
226
|
+
} else if (range.includes('-')) {
|
|
227
|
+
// IP range
|
|
228
|
+
if (this.isIPInRangeNotation(ip, range)) return true;
|
|
229
|
+
} else if (ip === range) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static isIPInCIDR(ip, cidr) {
|
|
237
|
+
// Simplified CIDR check - in production use a proper library
|
|
238
|
+
const [network, prefix] = cidr.split('/');
|
|
239
|
+
return ip.startsWith(network);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static isIPInRangeNotation(ip, range) {
|
|
243
|
+
const [start, end] = range.split('-');
|
|
244
|
+
return ip >= start && ip <= end;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class MinimalRouter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.routes = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
route(method, path, handler) {
|
|
7
|
+
const key = `${method}:${path}`;
|
|
8
|
+
this.routes.set(key, handler);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(path, handler) {
|
|
12
|
+
this.route('GET', path, handler);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
post(path, handler) {
|
|
16
|
+
this.route('POST', path, handler);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
put(path, handler) {
|
|
20
|
+
this.route('PUT', path, handler);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
delete(path, handler) {
|
|
24
|
+
this.route('DELETE', path, handler);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handle(context) {
|
|
28
|
+
const { request } = context;
|
|
29
|
+
const key = `${request.method}:${request.url.pathname}`;
|
|
30
|
+
|
|
31
|
+
const handler = this.routes.get(key);
|
|
32
|
+
if (!handler) {
|
|
33
|
+
throw new Error(`Route not found: ${key}`, 404);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await handler(context);
|
|
37
|
+
context.response = result;
|
|
38
|
+
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
}
|