webspresso 0.0.98 → 0.0.99
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/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +1 -1
- package/.agents/AUTH.md +169 -0
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +73 -5
- package/.agents/QUEUE.md +84 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +8 -0
- package/core/orm/model.js +4 -0
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +82 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/index.d.ts +528 -0
- package/index.js +44 -0
- package/package.json +1 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +6 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/src/api/api-loader.js +1 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +137 -100
- package/src/helpers.js +9 -0
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Plugin for Webspresso
|
|
3
|
+
* Zero-dependency B2B & Developer API Key Management
|
|
4
|
+
* @module plugins/api-key
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { safeCompare, hashKey, maskKey, generateKey } = require('./crypto');
|
|
10
|
+
const { hasScope, hasRequiredScopes, normalizeScopes, matchScope } = require('./scopes');
|
|
11
|
+
const { createApiKeyModel } = require('./model');
|
|
12
|
+
const { generateApiKeysMigration } = require('./migration-template');
|
|
13
|
+
const {
|
|
14
|
+
createApiKeyMiddleware,
|
|
15
|
+
extractApiKey,
|
|
16
|
+
sendUnauthorized,
|
|
17
|
+
sendForbidden,
|
|
18
|
+
resolveKnex,
|
|
19
|
+
} = require('./middleware');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Programmatic helper to create an API key record
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} [data]
|
|
25
|
+
* @param {string} [data.name='Default API Key']
|
|
26
|
+
* @param {string|string[]} [data.scopes=[]]
|
|
27
|
+
* @param {number|string|null} [data.userId]
|
|
28
|
+
* @param {number|string|null} [data.user_id]
|
|
29
|
+
* @param {Date|string|null} [data.expiresAt]
|
|
30
|
+
* @param {Date|string|null} [data.expires_at]
|
|
31
|
+
* @param {number|null} [data.rateLimit]
|
|
32
|
+
* @param {number|null} [data.rate_limit]
|
|
33
|
+
* @param {any} [data.metadata]
|
|
34
|
+
* @param {string} [data.prefix]
|
|
35
|
+
* @param {number} [data.byteLength]
|
|
36
|
+
* @param {Object} [options]
|
|
37
|
+
* @returns {Promise<{ key: string, record: Object }>}
|
|
38
|
+
*/
|
|
39
|
+
async function createApiKey(data = {}, options = {}) {
|
|
40
|
+
const prefix = data.prefix || options.prefix || 'wsp_live_';
|
|
41
|
+
const byteLength = data.byteLength || options.byteLength || 24;
|
|
42
|
+
const tableName = options.tableName || 'api_keys';
|
|
43
|
+
|
|
44
|
+
const generated = generateKey({ prefix, byteLength });
|
|
45
|
+
const rawKey = generated.key;
|
|
46
|
+
const keyHash = generated.hash;
|
|
47
|
+
const keyHint = generated.hint;
|
|
48
|
+
|
|
49
|
+
const scopesList = normalizeScopes(data.scopes);
|
|
50
|
+
const userId = data.userId !== undefined ? data.userId : (data.user_id !== undefined ? data.user_id : null);
|
|
51
|
+
const expiresAt = data.expiresAt || data.expires_at || null;
|
|
52
|
+
const rateLimit = data.rateLimit !== undefined ? data.rateLimit : (data.rate_limit !== undefined ? data.rate_limit : null);
|
|
53
|
+
const metadata = data.metadata || null;
|
|
54
|
+
|
|
55
|
+
const recordData = {
|
|
56
|
+
name: data.name || 'Default API Key',
|
|
57
|
+
key_hash: keyHash,
|
|
58
|
+
key_hint: keyHint,
|
|
59
|
+
prefix,
|
|
60
|
+
user_id: userId,
|
|
61
|
+
scopes: JSON.stringify(scopesList),
|
|
62
|
+
rate_limit: rateLimit,
|
|
63
|
+
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
64
|
+
expires_at: expiresAt ? new Date(expiresAt) : null,
|
|
65
|
+
last_used_at: null,
|
|
66
|
+
revoked_at: null,
|
|
67
|
+
created_at: new Date(),
|
|
68
|
+
updated_at: new Date(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const kx = resolveKnex(options);
|
|
72
|
+
if (kx) {
|
|
73
|
+
const insertRes = await kx(tableName).insert(recordData);
|
|
74
|
+
let insertedId = 1;
|
|
75
|
+
if (Array.isArray(insertRes) && insertRes.length > 0) {
|
|
76
|
+
insertedId = typeof insertRes[0] === 'object' && insertRes[0] !== null ? insertRes[0].id : insertRes[0];
|
|
77
|
+
} else if (typeof insertRes === 'number') {
|
|
78
|
+
insertedId = insertRes;
|
|
79
|
+
}
|
|
80
|
+
recordData.id = insertedId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
key: rawKey, // Plaintext API key — returned ONLY once at creation!
|
|
85
|
+
record: {
|
|
86
|
+
...recordData,
|
|
87
|
+
scopes: scopesList,
|
|
88
|
+
metadata,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Programmatic helper to verify a raw API key against database or options
|
|
95
|
+
*
|
|
96
|
+
* @param {string} rawKey
|
|
97
|
+
* @param {Object} [options]
|
|
98
|
+
* @returns {Promise<Object|null>}
|
|
99
|
+
*/
|
|
100
|
+
async function verifyApiKey(rawKey, options = {}) {
|
|
101
|
+
if (!rawKey || typeof rawKey !== 'string') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hash = hashKey(rawKey);
|
|
106
|
+
const tableName = options.tableName || 'api_keys';
|
|
107
|
+
const kx = resolveKnex(options);
|
|
108
|
+
|
|
109
|
+
let record = null;
|
|
110
|
+
if (options.findKeyByHash) {
|
|
111
|
+
record = await options.findKeyByHash(hash);
|
|
112
|
+
} else if (options.verify) {
|
|
113
|
+
record = await options.verify(rawKey);
|
|
114
|
+
} else if (kx) {
|
|
115
|
+
record = await kx(tableName).where({ key_hash: hash }).first();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!record) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (record.key_hash && !safeCompare(record.key_hash, hash)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (record.revoked_at || record.is_active === false || record.revoked === true) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (record.expires_at) {
|
|
131
|
+
const expiresTime = new Date(record.expires_at).getTime();
|
|
132
|
+
if (!isNaN(expiresTime) && expiresTime <= Date.now()) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return record;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Programmatic helper to revoke an API key by ID or SHA-256 hash
|
|
142
|
+
*
|
|
143
|
+
* @param {number|string} idOrHash
|
|
144
|
+
* @param {Object} [options]
|
|
145
|
+
* @returns {Promise<boolean>}
|
|
146
|
+
*/
|
|
147
|
+
async function revokeApiKey(idOrHash, options = {}) {
|
|
148
|
+
const tableName = options.tableName || 'api_keys';
|
|
149
|
+
const kx = resolveKnex(options);
|
|
150
|
+
if (!kx) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const isHash = typeof idOrHash === 'string' && idOrHash.length === 64 && /^[0-9a-fA-F]+$/.test(idOrHash);
|
|
155
|
+
const condition = isHash ? { key_hash: idOrHash } : { id: idOrHash };
|
|
156
|
+
|
|
157
|
+
const updatedRows = await kx(tableName).where(condition).update({
|
|
158
|
+
revoked_at: new Date(),
|
|
159
|
+
updated_at: new Date(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return updatedRows > 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Webspresso API Key Plugin Factory
|
|
167
|
+
*
|
|
168
|
+
* @param {Object} [options] - Plugin configuration options
|
|
169
|
+
* @param {any} [options.db] - Webspresso database or Knex instance
|
|
170
|
+
* @param {any} [options.knex] - Knex instance
|
|
171
|
+
* @param {string} [options.tableName='api_keys'] - API keys table name
|
|
172
|
+
* @param {string} [options.prefix='wsp_live_'] - Default prefix for generated keys
|
|
173
|
+
* @param {number} [options.byteLength=24] - Random byte length for keys
|
|
174
|
+
* @param {boolean} [options.global=false] - Whether to apply API key auth globally
|
|
175
|
+
* @param {string|string[]|boolean} [options.routes=true] - Route prefixes (when global: true)
|
|
176
|
+
* @param {string[]} [options.skipPaths] - Array of path prefixes to skip
|
|
177
|
+
* @param {Function} [options.skip] - Custom predicate function (req) => boolean
|
|
178
|
+
* @param {Array|Object} [options.keys] - Static in-memory keys for testing / serverless
|
|
179
|
+
* @param {boolean} [options.trackUsage=true] - Automatically update last_used_at
|
|
180
|
+
* @param {number} [options.updateLastUsedThrottleMs=60000] - Throttle time for last_used_at writes
|
|
181
|
+
* @param {boolean} [options.allowQueryParam=false] - Allow query parameter extraction (?apiKey=...)
|
|
182
|
+
* @param {string} [options.queryParamName='apiKey'] - Query parameter name
|
|
183
|
+
* @param {Function} [options.findKeyByHash] - Custom lookup (hash, req) => Promise<record|null>
|
|
184
|
+
* @param {Function} [options.verify] - Custom verification (rawKey, req) => Promise<record|null>
|
|
185
|
+
* @param {Function} [options.getUser] - Custom user loader (record, req) => Promise<user|null>
|
|
186
|
+
* @param {Function} [options.onAuthenticated] - Callback (req, record) => void
|
|
187
|
+
* @param {Function|string|Object} [options.unauthorizedResponse] - Custom 401 response
|
|
188
|
+
* @param {Function|string|Object} [options.forbiddenResponse] - Custom 403 response
|
|
189
|
+
* @returns {Object} Webspresso Plugin Definition
|
|
190
|
+
*/
|
|
191
|
+
function apiKeyPlugin(options = {}) {
|
|
192
|
+
const { global: applyGlobal = false, ...pluginDefaults } = options;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name: 'api-key',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
description: 'B2B & Developer API Key Management Plugin with named middleware, scopes, and constant-time verification',
|
|
198
|
+
options,
|
|
199
|
+
|
|
200
|
+
api: {
|
|
201
|
+
createMiddleware: (routeOpts = {}) =>
|
|
202
|
+
createApiKeyMiddleware({ ...pluginDefaults, ...routeOpts }),
|
|
203
|
+
generateKey: (opts = {}) =>
|
|
204
|
+
generateKey({ prefix: pluginDefaults.prefix, byteLength: pluginDefaults.byteLength, ...opts }),
|
|
205
|
+
hashKey,
|
|
206
|
+
maskKey: (rawKey, pfx) => maskKey(rawKey, pfx || pluginDefaults.prefix),
|
|
207
|
+
safeCompare,
|
|
208
|
+
hasScope,
|
|
209
|
+
hasRequiredScopes,
|
|
210
|
+
normalizeScopes,
|
|
211
|
+
matchScope,
|
|
212
|
+
createApiKey: (data, opts = {}) =>
|
|
213
|
+
createApiKey(data, { ...pluginDefaults, ...opts }),
|
|
214
|
+
verifyApiKey: (rawKey, opts = {}) =>
|
|
215
|
+
verifyApiKey(rawKey, { ...pluginDefaults, ...opts }),
|
|
216
|
+
revokeApiKey: (idOrHash, opts = {}) =>
|
|
217
|
+
revokeApiKey(idOrHash, { ...pluginDefaults, ...opts }),
|
|
218
|
+
getMigrationTemplate: (name) =>
|
|
219
|
+
generateApiKeysMigration(name || pluginDefaults.tableName || 'api_keys'),
|
|
220
|
+
getModel: (name) =>
|
|
221
|
+
createApiKeyModel(name || pluginDefaults.tableName || 'api_keys'),
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
register(ctx) {
|
|
225
|
+
// 1. Optionally register ApiKey ORM Model if DB is available and supports registerModel
|
|
226
|
+
const db = options.db || (ctx && ctx.db);
|
|
227
|
+
if (db && typeof db.registerModel === 'function' && typeof db.hasModel === 'function') {
|
|
228
|
+
const modelName = 'ApiKey';
|
|
229
|
+
if (!db.hasModel(modelName)) {
|
|
230
|
+
const modelDef = createApiKeyModel(pluginDefaults.tableName || 'api_keys');
|
|
231
|
+
db.registerModel(modelDef);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 2. Register named middleware factory: middleware: ['apiKey'] or [['apiKey', { scopes: [...] }]]
|
|
236
|
+
if (ctx.middlewares) {
|
|
237
|
+
ctx.middlewares.apiKey = (routeOpts = {}) =>
|
|
238
|
+
createApiKeyMiddleware({ ...pluginDefaults, ...routeOpts });
|
|
239
|
+
ctx.middlewares.apiKeyAuth = ctx.middlewares.apiKey;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 3. Mount global middleware if global: true
|
|
243
|
+
if (applyGlobal && ctx.app) {
|
|
244
|
+
const globalMw = createApiKeyMiddleware(options);
|
|
245
|
+
ctx.app.use(globalMw);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
apiKeyPlugin.apiKeyPlugin = apiKeyPlugin;
|
|
252
|
+
apiKeyPlugin.createApiKeyMiddleware = createApiKeyMiddleware;
|
|
253
|
+
apiKeyPlugin.generateKey = generateKey;
|
|
254
|
+
apiKeyPlugin.hashKey = hashKey;
|
|
255
|
+
apiKeyPlugin.maskKey = maskKey;
|
|
256
|
+
apiKeyPlugin.safeCompare = safeCompare;
|
|
257
|
+
apiKeyPlugin.hasScope = hasScope;
|
|
258
|
+
apiKeyPlugin.hasRequiredScopes = hasRequiredScopes;
|
|
259
|
+
apiKeyPlugin.normalizeScopes = normalizeScopes;
|
|
260
|
+
apiKeyPlugin.matchScope = matchScope;
|
|
261
|
+
apiKeyPlugin.createApiKeyModel = createApiKeyModel;
|
|
262
|
+
apiKeyPlugin.generateApiKeysMigration = generateApiKeysMigration;
|
|
263
|
+
apiKeyPlugin.extractApiKey = extractApiKey;
|
|
264
|
+
apiKeyPlugin.createApiKey = createApiKey;
|
|
265
|
+
apiKeyPlugin.verifyApiKey = verifyApiKey;
|
|
266
|
+
apiKeyPlugin.revokeApiKey = revokeApiKey;
|
|
267
|
+
apiKeyPlugin.sendUnauthorized = sendUnauthorized;
|
|
268
|
+
apiKeyPlugin.sendForbidden = sendForbidden;
|
|
269
|
+
|
|
270
|
+
module.exports = apiKeyPlugin;
|
|
271
|
+
module.exports.apiKeyPlugin = apiKeyPlugin;
|
|
272
|
+
module.exports.createApiKeyMiddleware = createApiKeyMiddleware;
|
|
273
|
+
module.exports.generateKey = generateKey;
|
|
274
|
+
module.exports.hashKey = hashKey;
|
|
275
|
+
module.exports.maskKey = maskKey;
|
|
276
|
+
module.exports.safeCompare = safeCompare;
|
|
277
|
+
module.exports.hasScope = hasScope;
|
|
278
|
+
module.exports.hasRequiredScopes = hasRequiredScopes;
|
|
279
|
+
module.exports.normalizeScopes = normalizeScopes;
|
|
280
|
+
module.exports.matchScope = matchScope;
|
|
281
|
+
module.exports.createApiKeyModel = createApiKeyModel;
|
|
282
|
+
module.exports.generateApiKeysMigration = generateApiKeysMigration;
|
|
283
|
+
module.exports.extractApiKey = extractApiKey;
|
|
284
|
+
module.exports.createApiKey = createApiKey;
|
|
285
|
+
module.exports.verifyApiKey = verifyApiKey;
|
|
286
|
+
module.exports.revokeApiKey = revokeApiKey;
|
|
287
|
+
module.exports.sendUnauthorized = sendUnauthorized;
|
|
288
|
+
module.exports.sendForbidden = sendForbidden;
|