webspresso 0.0.97 → 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.
Files changed (136) hide show
  1. package/.agents/ADMIN.md +163 -5
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/ARCHITECTURE.md +10 -0
  4. package/.agents/AUTH.md +169 -0
  5. package/.agents/CLI.md +195 -1
  6. package/.agents/ORM.md +88 -4
  7. package/.agents/PLUGINS.md +221 -9
  8. package/.agents/QUEUE.md +127 -0
  9. package/.agents/ROUTING.md +127 -7
  10. package/AGENTS.md +1 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/analytics-prune.js +57 -0
  14. package/bin/commands/api.js +207 -15
  15. package/bin/commands/build.js +99 -0
  16. package/bin/commands/dev.js +64 -35
  17. package/bin/commands/doctor.js +26 -0
  18. package/bin/commands/email-prune.js +47 -0
  19. package/bin/commands/make-job.js +86 -0
  20. package/bin/commands/make-model.js +250 -0
  21. package/bin/commands/new.js +45 -28
  22. package/bin/commands/openapi.js +102 -0
  23. package/bin/commands/page.js +6 -3
  24. package/bin/commands/queue-prune.js +59 -0
  25. package/bin/commands/routes.js +52 -0
  26. package/bin/commands/service.js +6 -3
  27. package/bin/commands/typegen.js +106 -0
  28. package/bin/commands/upload-prune.js +79 -0
  29. package/bin/webspresso.js +22 -0
  30. package/core/applySchema.js +13 -6
  31. package/core/auth/hash.js +21 -12
  32. package/core/auth/index.js +60 -0
  33. package/core/auth/manager.js +134 -0
  34. package/core/auth/middleware.js +88 -0
  35. package/core/auth/oauth.js +517 -0
  36. package/core/auth/qrcode.js +499 -0
  37. package/core/auth/totp.js +504 -0
  38. package/core/compression/middleware.js +23 -17
  39. package/core/openapi/build-from-api-routes.js +160 -29
  40. package/core/orm/cache/fingerprint.js +15 -1
  41. package/core/orm/cache/memory-provider.js +27 -2
  42. package/core/orm/eager-loader.js +144 -10
  43. package/core/orm/index.js +20 -7
  44. package/core/orm/model.js +53 -7
  45. package/core/orm/query-builder.js +90 -4
  46. package/core/orm/repository.js +44 -3
  47. package/core/orm/search.js +213 -0
  48. package/core/queue/adapters/database.js +94 -0
  49. package/core/queue/adapters/memory.js +62 -0
  50. package/core/queue/adapters/redis.js +80 -0
  51. package/core/queue/cron.js +331 -0
  52. package/core/queue/index.js +8 -1
  53. package/core/queue/manager.js +205 -1
  54. package/core/queue/scheduler.js +359 -0
  55. package/core/sse/index.js +19 -0
  56. package/core/sse/stream.js +430 -0
  57. package/core/ssr/stream.js +11 -0
  58. package/index.d.ts +1188 -10
  59. package/index.js +88 -96
  60. package/package.json +1 -1
  61. package/plugins/admin-panel/api.js +279 -17
  62. package/plugins/admin-panel/app.js +114 -22
  63. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  64. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  65. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
  66. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  67. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  68. package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
  69. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  70. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  71. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  72. package/plugins/admin-panel/core/admin-module.js +13 -7
  73. package/plugins/admin-panel/core/api-extensions.js +30 -2
  74. package/plugins/admin-panel/core/registry.js +96 -16
  75. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  76. package/plugins/admin-panel/field-renderers/index.js +2 -0
  77. package/plugins/admin-panel/index.js +96 -0
  78. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  79. package/plugins/admin-panel/modules/admin-users.js +16 -1
  80. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  81. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  82. package/plugins/admin-panel/modules/dashboard.js +283 -0
  83. package/plugins/admin-panel/modules/menu.js +56 -18
  84. package/plugins/admin-panel/modules/user-management.js +24 -6
  85. package/plugins/api-key/crypto.js +101 -0
  86. package/plugins/api-key/index.js +288 -0
  87. package/plugins/api-key/middleware.js +461 -0
  88. package/plugins/api-key/migration-template.js +48 -0
  89. package/plugins/api-key/model.js +50 -0
  90. package/plugins/api-key/scopes.js +125 -0
  91. package/plugins/audit-log/admin-component.js +487 -59
  92. package/plugins/audit-log/api-handlers.js +148 -0
  93. package/plugins/audit-log/index.js +153 -3
  94. package/plugins/audit-log/purge.js +60 -10
  95. package/plugins/dashboard/app.js +152 -27
  96. package/plugins/dashboard/index.js +143 -39
  97. package/plugins/dashboard/styles.js +78 -0
  98. package/plugins/data-exchange/export-xlsx.js +9 -2
  99. package/plugins/data-exchange/import.js +9 -2
  100. package/plugins/data-exchange/index.js +6 -3
  101. package/plugins/email/index.js +132 -2
  102. package/plugins/email/purge.js +60 -10
  103. package/plugins/file-manager/admin-component.js +20 -4
  104. package/plugins/file-manager/index.js +21 -4
  105. package/plugins/index.js +10 -0
  106. package/plugins/oauth/index.js +338 -0
  107. package/plugins/orm-cache-admin/index.js +4 -2
  108. package/plugins/queue/index.js +15 -0
  109. package/plugins/schema-explorer/admin-component.js +714 -0
  110. package/plugins/schema-explorer/relations-graph.js +205 -0
  111. package/plugins/schema-explorer.js +248 -69
  112. package/plugins/site-analytics/admin-component.js +23 -0
  113. package/plugins/site-analytics/api-handlers.js +42 -0
  114. package/plugins/site-analytics/index.js +147 -2
  115. package/plugins/site-analytics/purge.js +168 -0
  116. package/plugins/swagger.js +261 -13
  117. package/plugins/upload/index.js +26 -0
  118. package/plugins/upload/orphan-pruner.js +292 -0
  119. package/src/api/api-loader.js +4 -1
  120. package/src/bundle/index.js +517 -0
  121. package/src/bundle/manifest.js +165 -0
  122. package/src/dev/banner.js +296 -0
  123. package/src/dev/colors.js +70 -0
  124. package/src/dev/index.js +62 -0
  125. package/src/dev/profiler.js +354 -0
  126. package/src/dev/typegen.js +497 -0
  127. package/src/dev-reload/index.js +611 -0
  128. package/src/discovery/file-route-parser.js +44 -1
  129. package/src/discovery/scan-routes.js +2 -2
  130. package/src/file-router.js +347 -162
  131. package/src/helpers.js +967 -98
  132. package/src/njk-frontmatter.js +12 -0
  133. package/src/pages/page-loader.js +61 -11
  134. package/src/routing/cascading.js +378 -0
  135. package/src/routing/middleware-resolver.js +92 -0
  136. package/src/server.js +635 -63
@@ -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;