naidejs 1.0.0 → 1.2.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/README.md +299 -3
- package/SPEC-X.nx +80 -7
- package/SPEC.naide +117 -9
- package/bin/naide.js +59 -2
- package/examples/fullapp.naide +74 -0
- package/examples/fullapp.nx +74 -0
- package/package.json +13 -5
- package/src/generator.js +493 -19
- package/src/index.js +2 -2
- package/src/parser.js +376 -73
- package/src/preprocess.js +8 -0
- package/src/runtime.js +425 -0
- package/src/tokens.js +24 -0
package/src/preprocess.js
CHANGED
|
@@ -107,6 +107,14 @@ export function preprocess(source) {
|
|
|
107
107
|
const rest = line.slice(1).trim();
|
|
108
108
|
if (rest.startsWith('.s ') || rest.startsWith('.s(')) {
|
|
109
109
|
out = 'ret.status ' + transformContent(rest.slice(3));
|
|
110
|
+
} else if (rest.startsWith('.r ')) {
|
|
111
|
+
out = 'ret.redirect ' + transformContent(rest.slice(3));
|
|
112
|
+
} else if (rest.startsWith('.h ')) {
|
|
113
|
+
out = 'ret.html ' + transformContent(rest.slice(3));
|
|
114
|
+
} else if (rest.startsWith('.t ')) {
|
|
115
|
+
out = 'ret.text ' + transformContent(rest.slice(3));
|
|
116
|
+
} else if (rest.startsWith('.f ')) {
|
|
117
|
+
out = 'ret.file ' + transformContent(rest.slice(3));
|
|
110
118
|
} else {
|
|
111
119
|
out = rest ? 'ret ' + transformContent(rest) : 'ret';
|
|
112
120
|
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { createHmac, randomUUID, timingSafeEqual, scryptSync, randomBytes } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
// ===== Password Hashing (zero-dep, Node crypto) =====
|
|
6
|
+
export function hash(password) {
|
|
7
|
+
const salt = randomBytes(16).toString('hex');
|
|
8
|
+
const derived = scryptSync(password, salt, 64).toString('hex');
|
|
9
|
+
return `${salt}:${derived}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function verify(password, stored) {
|
|
13
|
+
const [salt, h] = stored.split(':');
|
|
14
|
+
const hashBuf = Buffer.from(h, 'hex');
|
|
15
|
+
const testBuf = scryptSync(password, salt, 64);
|
|
16
|
+
return timingSafeEqual(hashBuf, testBuf);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ===== UUID =====
|
|
20
|
+
export function uuid() {
|
|
21
|
+
return randomUUID();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ===== Schema + Validation =====
|
|
25
|
+
export function createSchema(name, fieldDefs) {
|
|
26
|
+
const schema = {
|
|
27
|
+
name,
|
|
28
|
+
fields: fieldDefs,
|
|
29
|
+
validate(data) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const [field, rules] of Object.entries(fieldDefs)) {
|
|
33
|
+
let val = data[field];
|
|
34
|
+
if (rules.auto && rules.type === 'id' && val === undefined) { result[field] = randomUUID(); continue; }
|
|
35
|
+
if (rules.auto && rules.type === 'timestamp' && val === undefined) { result[field] = new Date().toISOString(); continue; }
|
|
36
|
+
if (val === undefined && rules.default !== undefined) val = rules.default;
|
|
37
|
+
if (rules.required && (val === undefined || val === null || val === '')) { errors.push(`${field} is required`); continue; }
|
|
38
|
+
if (val === undefined || val === null) continue;
|
|
39
|
+
if (rules.type === 'string') {
|
|
40
|
+
if (typeof val !== 'string') { errors.push(`${field} must be a string`); continue; }
|
|
41
|
+
if (rules.min !== undefined && val.length < rules.min) errors.push(`${field} must be at least ${rules.min} characters`);
|
|
42
|
+
if (rules.max !== undefined && val.length > rules.max) errors.push(`${field} must be at most ${rules.max} characters`);
|
|
43
|
+
if (rules.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) errors.push(`${field} must be a valid email`);
|
|
44
|
+
if (rules.url && !/^https?:\/\/.+/.test(val)) errors.push(`${field} must be a valid URL`);
|
|
45
|
+
if (rules.match && !rules.match.test(val)) errors.push(`${field} format is invalid`);
|
|
46
|
+
}
|
|
47
|
+
if (rules.type === 'number' || rules.type === 'integer') {
|
|
48
|
+
const n = Number(val);
|
|
49
|
+
if (isNaN(n)) { errors.push(`${field} must be a number`); continue; }
|
|
50
|
+
if (rules.type === 'integer' && !Number.isInteger(n)) errors.push(`${field} must be an integer`);
|
|
51
|
+
if (rules.min !== undefined && n < rules.min) errors.push(`${field} must be at least ${rules.min}`);
|
|
52
|
+
if (rules.max !== undefined && n > rules.max) errors.push(`${field} must be at most ${rules.max}`);
|
|
53
|
+
val = n;
|
|
54
|
+
}
|
|
55
|
+
if (rules.type === 'boolean') val = Boolean(val);
|
|
56
|
+
if (rules.type === 'enum' && !rules.values.includes(val)) errors.push(`${field} must be one of: ${rules.values.join(', ')}`);
|
|
57
|
+
result[field] = val;
|
|
58
|
+
}
|
|
59
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true, data: result };
|
|
60
|
+
},
|
|
61
|
+
defaults() {
|
|
62
|
+
const d = {};
|
|
63
|
+
for (const [field, rules] of Object.entries(fieldDefs)) {
|
|
64
|
+
if (rules.default !== undefined) d[field] = rules.default;
|
|
65
|
+
if (rules.auto && rules.type === 'id') d[field] = randomUUID();
|
|
66
|
+
if (rules.auto && rules.type === 'timestamp') d[field] = new Date().toISOString();
|
|
67
|
+
}
|
|
68
|
+
return d;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return schema;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ===== In-Memory Store =====
|
|
75
|
+
export function createStore(schema) {
|
|
76
|
+
const items = new Map();
|
|
77
|
+
let _idField = null;
|
|
78
|
+
for (const [f, r] of Object.entries(schema.fields)) {
|
|
79
|
+
if (r.auto && r.type === 'id') { _idField = f; break; }
|
|
80
|
+
}
|
|
81
|
+
const idField = _idField || 'id';
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
getAll() { return [...items.values()]; },
|
|
85
|
+
getById(id) { return items.get(String(id)) || null; },
|
|
86
|
+
count() { return items.size; },
|
|
87
|
+
create(data) {
|
|
88
|
+
const { valid, errors, data: validated } = schema.validate(data);
|
|
89
|
+
if (!valid) return { error: errors };
|
|
90
|
+
const id = validated[idField] || randomUUID();
|
|
91
|
+
validated[idField] = id;
|
|
92
|
+
items.set(String(id), validated);
|
|
93
|
+
return validated;
|
|
94
|
+
},
|
|
95
|
+
update(id, data) {
|
|
96
|
+
const existing = items.get(String(id));
|
|
97
|
+
if (!existing) return null;
|
|
98
|
+
const merged = { ...existing, ...data, [idField]: existing[idField] };
|
|
99
|
+
items.set(String(id), merged);
|
|
100
|
+
return merged;
|
|
101
|
+
},
|
|
102
|
+
delete(id) {
|
|
103
|
+
return items.delete(String(id));
|
|
104
|
+
},
|
|
105
|
+
where(conditions) {
|
|
106
|
+
return [...items.values()].filter(item => {
|
|
107
|
+
for (const [k, v] of Object.entries(conditions)) {
|
|
108
|
+
if (item[k] !== v) return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
clear() { items.clear(); }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ===== File-Based Persistent Store =====
|
|
118
|
+
export function createFileStore(schema, filePath) {
|
|
119
|
+
let _idField = null;
|
|
120
|
+
for (const [f, r] of Object.entries(schema.fields)) {
|
|
121
|
+
if (r.auto && r.type === 'id') { _idField = f; break; }
|
|
122
|
+
}
|
|
123
|
+
const idField = _idField || 'id';
|
|
124
|
+
|
|
125
|
+
const dir = dirname(filePath);
|
|
126
|
+
if (dir && dir !== '.' && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
127
|
+
|
|
128
|
+
const items = new Map();
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
131
|
+
for (const item of data) items.set(String(item[idField] || item.id), item);
|
|
132
|
+
} catch {}
|
|
133
|
+
|
|
134
|
+
function save() {
|
|
135
|
+
writeFileSync(filePath, JSON.stringify([...items.values()], null, 2));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
getAll() { return [...items.values()]; },
|
|
140
|
+
getById(id) { return items.get(String(id)) || null; },
|
|
141
|
+
count() { return items.size; },
|
|
142
|
+
create(data) {
|
|
143
|
+
const { valid, errors, data: validated } = schema.validate(data);
|
|
144
|
+
if (!valid) return { error: errors };
|
|
145
|
+
const id = validated[idField] || randomUUID();
|
|
146
|
+
validated[idField] = id;
|
|
147
|
+
items.set(String(id), validated);
|
|
148
|
+
save();
|
|
149
|
+
return validated;
|
|
150
|
+
},
|
|
151
|
+
update(id, data) {
|
|
152
|
+
const existing = items.get(String(id));
|
|
153
|
+
if (!existing) return null;
|
|
154
|
+
const merged = { ...existing, ...data, [idField]: existing[idField] };
|
|
155
|
+
items.set(String(id), merged);
|
|
156
|
+
save();
|
|
157
|
+
return merged;
|
|
158
|
+
},
|
|
159
|
+
delete(id) {
|
|
160
|
+
const result = items.delete(String(id));
|
|
161
|
+
if (result) save();
|
|
162
|
+
return result;
|
|
163
|
+
},
|
|
164
|
+
where(conditions) {
|
|
165
|
+
return [...items.values()].filter(item => {
|
|
166
|
+
for (const [k, v] of Object.entries(conditions)) {
|
|
167
|
+
if (item[k] !== v) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
clear() { items.clear(); save(); }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ===== CRUD Route Registration (with pagination, search, sort) =====
|
|
177
|
+
export function registerCrud(app, basePath, schema, store, eventBus) {
|
|
178
|
+
app.get(basePath, (req, res) => {
|
|
179
|
+
let items = store.getAll();
|
|
180
|
+
|
|
181
|
+
const q = req.query.q;
|
|
182
|
+
if (q) {
|
|
183
|
+
const lower = q.toLowerCase();
|
|
184
|
+
items = items.filter(item =>
|
|
185
|
+
Object.values(item).some(v =>
|
|
186
|
+
typeof v === 'string' && v.toLowerCase().includes(lower)
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (req.query.sort) {
|
|
192
|
+
const field = req.query.sort;
|
|
193
|
+
const order = req.query.order === 'desc' ? -1 : 1;
|
|
194
|
+
items.sort((a, b) => {
|
|
195
|
+
if (a[field] < b[field]) return -order;
|
|
196
|
+
if (a[field] > b[field]) return order;
|
|
197
|
+
return 0;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const total = items.length;
|
|
202
|
+
const page = parseInt(req.query.page) || 1;
|
|
203
|
+
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
204
|
+
items = items.slice((page - 1) * limit, page * limit);
|
|
205
|
+
|
|
206
|
+
res.json({ data: items, total, page, limit, pages: Math.ceil(total / limit) });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
app.get(`${basePath}/:id`, (req, res) => {
|
|
210
|
+
const item = store.getById(req.params.id);
|
|
211
|
+
if (!item) return res.status(404).json({ error: `${schema.name} not found` });
|
|
212
|
+
res.json(item);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
app.post(basePath, (req, res) => {
|
|
216
|
+
const result = store.create(req.body);
|
|
217
|
+
if (result.error) return res.status(400).json({ errors: result.error });
|
|
218
|
+
if (eventBus) eventBus.emit(`${schema.name}.create`, result);
|
|
219
|
+
res.status(201).json(result);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.put(`${basePath}/:id`, (req, res) => {
|
|
223
|
+
const item = store.getById(req.params.id);
|
|
224
|
+
if (!item) return res.status(404).json({ error: `${schema.name} not found` });
|
|
225
|
+
const updated = store.update(req.params.id, req.body);
|
|
226
|
+
if (eventBus) eventBus.emit(`${schema.name}.update`, updated);
|
|
227
|
+
res.json(updated);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
app.delete(`${basePath}/:id`, (req, res) => {
|
|
231
|
+
const item = store.getById(req.params.id);
|
|
232
|
+
if (!item) return res.status(404).json({ error: `${schema.name} not found` });
|
|
233
|
+
store.delete(req.params.id);
|
|
234
|
+
if (eventBus) eventBus.emit(`${schema.name}.delete`, { id: req.params.id });
|
|
235
|
+
res.json({ deleted: true });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ===== JWT Auth =====
|
|
240
|
+
function base64url(buf) {
|
|
241
|
+
return Buffer.from(buf).toString('base64url');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function jwtSign(payload, secret, expiresIn = '24h') {
|
|
245
|
+
const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
246
|
+
const ms = typeof expiresIn === 'number' ? expiresIn : parseMs(expiresIn);
|
|
247
|
+
const body = base64url(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000), exp: Math.floor((Date.now() + ms) / 1000) }));
|
|
248
|
+
const sig = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
|
|
249
|
+
return `${header}.${body}.${sig}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function jwtVerify(token, secret) {
|
|
253
|
+
const parts = token.split('.');
|
|
254
|
+
if (parts.length !== 3) throw new Error('Invalid token');
|
|
255
|
+
const sig = createHmac('sha256', secret).update(`${parts[0]}.${parts[1]}`).digest('base64url');
|
|
256
|
+
const sigBuf = Buffer.from(sig);
|
|
257
|
+
const tokenSigBuf = Buffer.from(parts[2]);
|
|
258
|
+
if (sigBuf.length !== tokenSigBuf.length || !timingSafeEqual(sigBuf, tokenSigBuf)) throw new Error('Invalid signature');
|
|
259
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
260
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) throw new Error('Token expired');
|
|
261
|
+
return payload;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function jwtAuth(secret, options = {}) {
|
|
265
|
+
const publicPaths = (options.public || []).map(p => new RegExp('^' + p.replace(/\*/g, '.*') + '$'));
|
|
266
|
+
return (req, res, next) => {
|
|
267
|
+
const isPublic = publicPaths.some(re => re.test(req.path));
|
|
268
|
+
if (isPublic) return next();
|
|
269
|
+
const authHeader = req.headers.authorization;
|
|
270
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ error: 'No token provided' });
|
|
271
|
+
try {
|
|
272
|
+
req.user = jwtVerify(authHeader.slice(7), secret);
|
|
273
|
+
next();
|
|
274
|
+
} catch (e) {
|
|
275
|
+
res.status(403).json({ error: e.message });
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ===== CORS Middleware =====
|
|
281
|
+
export function corsMiddleware(origins = ['*']) {
|
|
282
|
+
const allowAll = origins.includes('*');
|
|
283
|
+
const originSet = new Set(origins.map(o => o.replace(/^https?:\/\//, '')));
|
|
284
|
+
return (req, res, next) => {
|
|
285
|
+
const reqOrigin = req.headers.origin || '';
|
|
286
|
+
const host = reqOrigin.replace(/^https?:\/\//, '');
|
|
287
|
+
if (allowAll || originSet.has(host)) {
|
|
288
|
+
res.setHeader('Access-Control-Allow-Origin', reqOrigin || '*');
|
|
289
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
|
|
290
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
|
|
291
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
292
|
+
}
|
|
293
|
+
if (req.method === 'OPTIONS') return res.status(204).end();
|
|
294
|
+
next();
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ===== Rate Limiting =====
|
|
299
|
+
export function rateLimit(max, window = '1m') {
|
|
300
|
+
const windowMs = parseMs(window);
|
|
301
|
+
const hits = new Map();
|
|
302
|
+
setInterval(() => hits.clear(), windowMs);
|
|
303
|
+
return (req, res, next) => {
|
|
304
|
+
const key = req.ip;
|
|
305
|
+
const count = (hits.get(key) || 0) + 1;
|
|
306
|
+
hits.set(key, count);
|
|
307
|
+
res.setHeader('X-RateLimit-Limit', max);
|
|
308
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count));
|
|
309
|
+
if (count > max) return res.status(429).json({ error: 'Too many requests' });
|
|
310
|
+
next();
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ===== Env =====
|
|
315
|
+
export function loadEnv(spec) {
|
|
316
|
+
const env = {};
|
|
317
|
+
for (const [key, rules] of Object.entries(spec)) {
|
|
318
|
+
let val = process.env[key];
|
|
319
|
+
if (val === undefined && rules.default !== undefined) val = String(rules.default);
|
|
320
|
+
if (rules.required && val === undefined) {
|
|
321
|
+
console.error(`[NAIDE] Missing required env var: ${key}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
if (val !== undefined) {
|
|
325
|
+
if (rules.type === 'number' || rules.type === 'integer') val = Number(val);
|
|
326
|
+
if (rules.type === 'boolean') val = val === 'true' || val === '1';
|
|
327
|
+
}
|
|
328
|
+
env[key] = val;
|
|
329
|
+
}
|
|
330
|
+
return env;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ===== Cron / Scheduler =====
|
|
334
|
+
export function scheduleEvery(interval, fn) {
|
|
335
|
+
const ms = parseMs(interval);
|
|
336
|
+
const timer = setInterval(async () => {
|
|
337
|
+
try { await fn(); } catch (e) { console.error('[NAIDE Cron]', e.message); }
|
|
338
|
+
}, ms);
|
|
339
|
+
fn();
|
|
340
|
+
return timer;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ===== Event Bus (for watch) =====
|
|
344
|
+
export function createEventBus() {
|
|
345
|
+
const listeners = new Map();
|
|
346
|
+
return {
|
|
347
|
+
on(event, fn) {
|
|
348
|
+
if (!listeners.has(event)) listeners.set(event, []);
|
|
349
|
+
listeners.get(event).push(fn);
|
|
350
|
+
},
|
|
351
|
+
emit(event, data) {
|
|
352
|
+
const fns = listeners.get(event) || [];
|
|
353
|
+
for (const fn of fns) {
|
|
354
|
+
try { fn({ event, data, timestamp: new Date().toISOString() }); } catch (e) { console.error('[NAIDE Event]', e.message); }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ===== HTTP Client (zero-dep, Node 18+ fetch) =====
|
|
361
|
+
async function jsonOrText(res) {
|
|
362
|
+
const ct = res.headers.get('content-type') || '';
|
|
363
|
+
return ct.includes('json') ? res.json() : res.text();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export const api = {
|
|
367
|
+
async get(url, opts = {}) {
|
|
368
|
+
const res = await fetch(url, { ...opts, method: 'GET' });
|
|
369
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
370
|
+
return jsonOrText(res);
|
|
371
|
+
},
|
|
372
|
+
async post(url, body, opts = {}) {
|
|
373
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...opts.headers }, body: JSON.stringify(body), ...opts });
|
|
374
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
375
|
+
return jsonOrText(res);
|
|
376
|
+
},
|
|
377
|
+
async put(url, body, opts = {}) {
|
|
378
|
+
const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...opts.headers }, body: JSON.stringify(body), ...opts });
|
|
379
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
380
|
+
return jsonOrText(res);
|
|
381
|
+
},
|
|
382
|
+
async del(url, opts = {}) {
|
|
383
|
+
const res = await fetch(url, { method: 'DELETE', ...opts });
|
|
384
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
385
|
+
return jsonOrText(res);
|
|
386
|
+
},
|
|
387
|
+
async raw(url, opts = {}) {
|
|
388
|
+
return fetch(url, opts);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// ===== Cookie Parser =====
|
|
393
|
+
export function cookieParser() {
|
|
394
|
+
return (req, res, next) => {
|
|
395
|
+
req.cookies = {};
|
|
396
|
+
const header = req.headers.cookie;
|
|
397
|
+
if (header) {
|
|
398
|
+
for (const pair of header.split(';')) {
|
|
399
|
+
const idx = pair.indexOf('=');
|
|
400
|
+
if (idx > 0) {
|
|
401
|
+
const k = pair.slice(0, idx).trim();
|
|
402
|
+
const v = pair.slice(idx + 1).trim();
|
|
403
|
+
try { req.cookies[k] = decodeURIComponent(v); } catch { req.cookies[k] = v; }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
next();
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ===== Helpers =====
|
|
412
|
+
function parseMs(str) {
|
|
413
|
+
if (typeof str === 'number') return str;
|
|
414
|
+
const m = str.match(/^(\d+)(ms|s|m|h|d)$/);
|
|
415
|
+
if (!m) return 60000;
|
|
416
|
+
const n = parseInt(m[1]);
|
|
417
|
+
switch (m[2]) {
|
|
418
|
+
case 'ms': return n;
|
|
419
|
+
case 's': return n * 1000;
|
|
420
|
+
case 'm': return n * 60000;
|
|
421
|
+
case 'h': return n * 3600000;
|
|
422
|
+
case 'd': return n * 86400000;
|
|
423
|
+
default: return 60000;
|
|
424
|
+
}
|
|
425
|
+
}
|
package/src/tokens.js
CHANGED
|
@@ -57,6 +57,18 @@ export const T = {
|
|
|
57
57
|
BREAK: 'BREAK',
|
|
58
58
|
CONTINUE: 'CONTINUE',
|
|
59
59
|
THROW: 'THROW',
|
|
60
|
+
SCHEMA: 'SCHEMA',
|
|
61
|
+
CRUD: 'CRUD',
|
|
62
|
+
AUTH: 'AUTH',
|
|
63
|
+
CORS: 'CORS',
|
|
64
|
+
LIMIT: 'LIMIT',
|
|
65
|
+
ENV: 'ENV',
|
|
66
|
+
EVERY: 'EVERY',
|
|
67
|
+
WATCH: 'WATCH',
|
|
68
|
+
STATIC: 'STATIC',
|
|
69
|
+
WS: 'WS',
|
|
70
|
+
GROUP: 'GROUP',
|
|
71
|
+
COOKIE: 'COOKIE',
|
|
60
72
|
|
|
61
73
|
// Operators
|
|
62
74
|
ASSIGN: 'ASSIGN',
|
|
@@ -142,6 +154,18 @@ export const KEYWORDS = {
|
|
|
142
154
|
'break': T.BREAK,
|
|
143
155
|
'continue': T.CONTINUE,
|
|
144
156
|
'throw': T.THROW,
|
|
157
|
+
'schema': T.SCHEMA,
|
|
158
|
+
'crud': T.CRUD,
|
|
159
|
+
'auth': T.AUTH,
|
|
160
|
+
'cors': T.CORS,
|
|
161
|
+
'limit': T.LIMIT,
|
|
162
|
+
'env': T.ENV,
|
|
163
|
+
'every': T.EVERY,
|
|
164
|
+
'watch': T.WATCH,
|
|
165
|
+
'static': T.STATIC,
|
|
166
|
+
'ws': T.WS,
|
|
167
|
+
'group': T.GROUP,
|
|
168
|
+
'cookie': T.COOKIE,
|
|
145
169
|
'true': T.BOOL,
|
|
146
170
|
'false': T.BOOL,
|
|
147
171
|
'null': T.NULL,
|