s3db.js 13.5.1 → 13.6.1
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 +89 -19
- package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24263 -18860
- package/dist/s3db.es.js.map +1 -1
- package/package.json +227 -21
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +514 -54
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +44 -11
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +262 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +32 -7
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +124 -32
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/tfstate/README.md +126 -126
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector
|
|
3
|
+
*
|
|
4
|
+
* Collects and aggregates API metrics for monitoring and observability.
|
|
5
|
+
*
|
|
6
|
+
* Metrics Collected:
|
|
7
|
+
* - Request counts by method, path, status
|
|
8
|
+
* - Request duration percentiles (p50, p95, p99)
|
|
9
|
+
* - Auth success/failure counts
|
|
10
|
+
* - Resource operation counts (created, updated, deleted)
|
|
11
|
+
* - User activity (logins, new users)
|
|
12
|
+
* - Error rates
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const metrics = new MetricsCollector({ enabled: true });
|
|
16
|
+
*
|
|
17
|
+
* // Record request
|
|
18
|
+
* metrics.recordRequest({
|
|
19
|
+
* method: 'GET',
|
|
20
|
+
* path: '/users',
|
|
21
|
+
* status: 200,
|
|
22
|
+
* duration: 45
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Get summary
|
|
26
|
+
* const summary = metrics.getSummary();
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export class MetricsCollector {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = {
|
|
32
|
+
enabled: options.enabled !== false, // Enabled by default
|
|
33
|
+
verbose: options.verbose || false,
|
|
34
|
+
maxPathsTracked: options.maxPathsTracked || 100, // Limit memory usage
|
|
35
|
+
resetInterval: options.resetInterval || 300000 // Reset every 5 minutes
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this.metrics = this._createEmptyMetrics();
|
|
39
|
+
this.startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// Auto-reset metrics periodically to prevent memory growth
|
|
42
|
+
if (this.options.resetInterval > 0) {
|
|
43
|
+
this.resetTimer = setInterval(() => {
|
|
44
|
+
if (this.options.verbose) {
|
|
45
|
+
console.log('[Metrics] Auto-resetting metrics');
|
|
46
|
+
}
|
|
47
|
+
this.reset();
|
|
48
|
+
}, this.options.resetInterval);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create empty metrics structure
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
_createEmptyMetrics() {
|
|
57
|
+
return {
|
|
58
|
+
requests: {
|
|
59
|
+
total: 0,
|
|
60
|
+
byMethod: {},
|
|
61
|
+
byStatus: {},
|
|
62
|
+
byPath: {},
|
|
63
|
+
durations: []
|
|
64
|
+
},
|
|
65
|
+
auth: {
|
|
66
|
+
success: 0,
|
|
67
|
+
failure: 0,
|
|
68
|
+
byMethod: {}
|
|
69
|
+
},
|
|
70
|
+
resources: {
|
|
71
|
+
created: 0,
|
|
72
|
+
updated: 0,
|
|
73
|
+
deleted: 0,
|
|
74
|
+
byResource: {}
|
|
75
|
+
},
|
|
76
|
+
users: {
|
|
77
|
+
logins: 0,
|
|
78
|
+
newUsers: 0
|
|
79
|
+
},
|
|
80
|
+
errors: {
|
|
81
|
+
total: 0,
|
|
82
|
+
byType: {}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record request metrics
|
|
89
|
+
* @param {Object} data - Request data
|
|
90
|
+
*/
|
|
91
|
+
recordRequest({ method, path, status, duration }) {
|
|
92
|
+
if (!this.options.enabled) return;
|
|
93
|
+
|
|
94
|
+
const metrics = this.metrics.requests;
|
|
95
|
+
|
|
96
|
+
metrics.total++;
|
|
97
|
+
|
|
98
|
+
// By method
|
|
99
|
+
metrics.byMethod[method] = (metrics.byMethod[method] || 0) + 1;
|
|
100
|
+
|
|
101
|
+
// By status
|
|
102
|
+
const statusGroup = `${Math.floor(status / 100)}xx`;
|
|
103
|
+
metrics.byStatus[statusGroup] = (metrics.byStatus[statusGroup] || 0) + 1;
|
|
104
|
+
|
|
105
|
+
// By path (limit tracking to prevent memory growth)
|
|
106
|
+
if (Object.keys(metrics.byPath).length < this.options.maxPathsTracked || metrics.byPath[path]) {
|
|
107
|
+
if (!metrics.byPath[path]) {
|
|
108
|
+
metrics.byPath[path] = { count: 0, totalDuration: 0, errors: 0 };
|
|
109
|
+
}
|
|
110
|
+
metrics.byPath[path].count++;
|
|
111
|
+
metrics.byPath[path].totalDuration += duration;
|
|
112
|
+
if (status >= 400) {
|
|
113
|
+
metrics.byPath[path].errors++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Store duration for percentile calculation
|
|
118
|
+
metrics.durations.push(duration);
|
|
119
|
+
|
|
120
|
+
// Keep only last 1000 durations to prevent memory growth
|
|
121
|
+
if (metrics.durations.length > 1000) {
|
|
122
|
+
metrics.durations.shift();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.options.verbose) {
|
|
126
|
+
console.log(`[Metrics] Request: ${method} ${path} ${status} (${duration}ms)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record auth metrics
|
|
132
|
+
* @param {Object} data - Auth data
|
|
133
|
+
*/
|
|
134
|
+
recordAuth({ success, method }) {
|
|
135
|
+
if (!this.options.enabled) return;
|
|
136
|
+
|
|
137
|
+
const metrics = this.metrics.auth;
|
|
138
|
+
|
|
139
|
+
if (success) {
|
|
140
|
+
metrics.success++;
|
|
141
|
+
} else {
|
|
142
|
+
metrics.failure++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// By method
|
|
146
|
+
if (!metrics.byMethod[method]) {
|
|
147
|
+
metrics.byMethod[method] = { success: 0, failure: 0 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (success) {
|
|
151
|
+
metrics.byMethod[method].success++;
|
|
152
|
+
} else {
|
|
153
|
+
metrics.byMethod[method].failure++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.options.verbose) {
|
|
157
|
+
console.log(`[Metrics] Auth: ${method} ${success ? 'success' : 'failure'}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Record resource operation metrics
|
|
163
|
+
* @param {Object} data - Resource operation data
|
|
164
|
+
*/
|
|
165
|
+
recordResourceOperation({ action, resource }) {
|
|
166
|
+
if (!this.options.enabled) return;
|
|
167
|
+
|
|
168
|
+
const metrics = this.metrics.resources;
|
|
169
|
+
|
|
170
|
+
// Total by action
|
|
171
|
+
if (action === 'created') metrics.created++;
|
|
172
|
+
else if (action === 'updated') metrics.updated++;
|
|
173
|
+
else if (action === 'deleted') metrics.deleted++;
|
|
174
|
+
|
|
175
|
+
// By resource
|
|
176
|
+
if (!metrics.byResource[resource]) {
|
|
177
|
+
metrics.byResource[resource] = { created: 0, updated: 0, deleted: 0 };
|
|
178
|
+
}
|
|
179
|
+
metrics.byResource[resource][action]++;
|
|
180
|
+
|
|
181
|
+
if (this.options.verbose) {
|
|
182
|
+
console.log(`[Metrics] Resource: ${resource} ${action}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Record user event metrics
|
|
188
|
+
* @param {Object} data - User event data
|
|
189
|
+
*/
|
|
190
|
+
recordUserEvent({ action }) {
|
|
191
|
+
if (!this.options.enabled) return;
|
|
192
|
+
|
|
193
|
+
const metrics = this.metrics.users;
|
|
194
|
+
|
|
195
|
+
if (action === 'login') {
|
|
196
|
+
metrics.logins++;
|
|
197
|
+
} else if (action === 'created') {
|
|
198
|
+
metrics.newUsers++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.options.verbose) {
|
|
202
|
+
console.log(`[Metrics] User: ${action}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Record error metrics
|
|
208
|
+
* @param {Object} data - Error data
|
|
209
|
+
*/
|
|
210
|
+
recordError({ error, type = 'unknown' }) {
|
|
211
|
+
if (!this.options.enabled) return;
|
|
212
|
+
|
|
213
|
+
const metrics = this.metrics.errors;
|
|
214
|
+
|
|
215
|
+
metrics.total++;
|
|
216
|
+
metrics.byType[type] = (metrics.byType[type] || 0) + 1;
|
|
217
|
+
|
|
218
|
+
if (this.options.verbose) {
|
|
219
|
+
console.log(`[Metrics] Error: ${type} - ${error}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Calculate percentile from sorted array
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
_percentile(arr, p) {
|
|
228
|
+
if (arr.length === 0) return 0;
|
|
229
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
230
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
231
|
+
return sorted[Math.max(0, index)];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get metrics summary
|
|
236
|
+
* @returns {Object} Metrics summary
|
|
237
|
+
*/
|
|
238
|
+
getSummary() {
|
|
239
|
+
const uptime = Date.now() - this.startTime;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
uptime: {
|
|
243
|
+
milliseconds: uptime,
|
|
244
|
+
seconds: Math.floor(uptime / 1000),
|
|
245
|
+
formatted: this._formatDuration(uptime)
|
|
246
|
+
},
|
|
247
|
+
requests: {
|
|
248
|
+
total: this.metrics.requests.total,
|
|
249
|
+
rps: (this.metrics.requests.total / (uptime / 1000)).toFixed(2),
|
|
250
|
+
byMethod: this.metrics.requests.byMethod,
|
|
251
|
+
byStatus: this.metrics.requests.byStatus,
|
|
252
|
+
topPaths: this._getTopPaths(),
|
|
253
|
+
duration: {
|
|
254
|
+
p50: this._percentile(this.metrics.requests.durations, 50),
|
|
255
|
+
p95: this._percentile(this.metrics.requests.durations, 95),
|
|
256
|
+
p99: this._percentile(this.metrics.requests.durations, 99),
|
|
257
|
+
avg: this.metrics.requests.durations.length > 0
|
|
258
|
+
? (this.metrics.requests.durations.reduce((a, b) => a + b, 0) / this.metrics.requests.durations.length).toFixed(2)
|
|
259
|
+
: 0
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
auth: {
|
|
263
|
+
total: this.metrics.auth.success + this.metrics.auth.failure,
|
|
264
|
+
success: this.metrics.auth.success,
|
|
265
|
+
failure: this.metrics.auth.failure,
|
|
266
|
+
successRate: this._calculateRate(this.metrics.auth.success, this.metrics.auth.success + this.metrics.auth.failure),
|
|
267
|
+
byMethod: this.metrics.auth.byMethod
|
|
268
|
+
},
|
|
269
|
+
resources: {
|
|
270
|
+
total: this.metrics.resources.created + this.metrics.resources.updated + this.metrics.resources.deleted,
|
|
271
|
+
created: this.metrics.resources.created,
|
|
272
|
+
updated: this.metrics.resources.updated,
|
|
273
|
+
deleted: this.metrics.resources.deleted,
|
|
274
|
+
byResource: this.metrics.resources.byResource
|
|
275
|
+
},
|
|
276
|
+
users: this.metrics.users,
|
|
277
|
+
errors: {
|
|
278
|
+
total: this.metrics.errors.total,
|
|
279
|
+
rate: this._calculateRate(this.metrics.errors.total, this.metrics.requests.total),
|
|
280
|
+
byType: this.metrics.errors.byType
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get top paths by request count
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
_getTopPaths(limit = 10) {
|
|
290
|
+
return Object.entries(this.metrics.requests.byPath)
|
|
291
|
+
.map(([path, data]) => ({
|
|
292
|
+
path,
|
|
293
|
+
count: data.count,
|
|
294
|
+
avgDuration: (data.totalDuration / data.count).toFixed(2),
|
|
295
|
+
errors: data.errors,
|
|
296
|
+
errorRate: this._calculateRate(data.errors, data.count)
|
|
297
|
+
}))
|
|
298
|
+
.sort((a, b) => b.count - a.count)
|
|
299
|
+
.slice(0, limit);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Calculate rate as percentage
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
_calculateRate(numerator, denominator) {
|
|
307
|
+
if (denominator === 0) return '0.00%';
|
|
308
|
+
return ((numerator / denominator) * 100).toFixed(2) + '%';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Format duration in human-readable form
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
_formatDuration(ms) {
|
|
316
|
+
const seconds = Math.floor(ms / 1000);
|
|
317
|
+
const minutes = Math.floor(seconds / 60);
|
|
318
|
+
const hours = Math.floor(minutes / 60);
|
|
319
|
+
const days = Math.floor(hours / 24);
|
|
320
|
+
|
|
321
|
+
if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
|
322
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
323
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
324
|
+
return `${seconds}s`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Reset metrics
|
|
329
|
+
*/
|
|
330
|
+
reset() {
|
|
331
|
+
this.metrics = this._createEmptyMetrics();
|
|
332
|
+
this.startTime = Date.now();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Stop metrics collection and cleanup
|
|
337
|
+
*/
|
|
338
|
+
stop() {
|
|
339
|
+
if (this.resetTimer) {
|
|
340
|
+
clearInterval(this.resetTimer);
|
|
341
|
+
this.resetTimer = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export default MetricsCollector;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenGraph Helper
|
|
3
|
+
*
|
|
4
|
+
* Generates OpenGraph and Twitter Card meta tags for social media previews.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const og = new OpenGraphHelper({
|
|
8
|
+
* siteName: 'My Site',
|
|
9
|
+
* locale: 'en_US',
|
|
10
|
+
* twitterSite: '@mysite'
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* const tags = og.generateTags({
|
|
14
|
+
* title: 'Page Title',
|
|
15
|
+
* description: 'Page description',
|
|
16
|
+
* image: '/og-image.jpg',
|
|
17
|
+
* url: 'https://example.com/page'
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
export class OpenGraphHelper {
|
|
21
|
+
constructor(defaults = {}) {
|
|
22
|
+
this.defaults = {
|
|
23
|
+
siteName: defaults.siteName || 'My Site',
|
|
24
|
+
locale: defaults.locale || 'en_US',
|
|
25
|
+
type: defaults.type || 'website',
|
|
26
|
+
twitterCard: defaults.twitterCard || 'summary_large_image',
|
|
27
|
+
twitterSite: defaults.twitterSite || null,
|
|
28
|
+
defaultImage: defaults.defaultImage || null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate OpenGraph meta tags
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} data - OpenGraph data
|
|
36
|
+
* @param {string} data.title - Page title
|
|
37
|
+
* @param {string} data.description - Page description
|
|
38
|
+
* @param {string} data.image - Image URL (absolute or relative)
|
|
39
|
+
* @param {string} data.url - Canonical URL
|
|
40
|
+
* @param {string} data.type - Content type (default: 'website')
|
|
41
|
+
* @param {string} data.locale - Locale (default: from defaults)
|
|
42
|
+
* @param {string} data.siteName - Site name (default: from defaults)
|
|
43
|
+
* @param {string} data.imageAlt - Image alt text
|
|
44
|
+
* @param {number} data.imageWidth - Image width in pixels
|
|
45
|
+
* @param {number} data.imageHeight - Image height in pixels
|
|
46
|
+
* @param {string} data.twitterCard - Twitter card type (default: from defaults)
|
|
47
|
+
* @param {string} data.twitterSite - Twitter @username (default: from defaults)
|
|
48
|
+
* @param {string} data.twitterCreator - Twitter creator @username
|
|
49
|
+
* @returns {string} HTML meta tags
|
|
50
|
+
*/
|
|
51
|
+
generateTags(data = {}) {
|
|
52
|
+
const og = { ...this.defaults, ...data };
|
|
53
|
+
|
|
54
|
+
// Use default image if none provided
|
|
55
|
+
const image = og.image || og.defaultImage;
|
|
56
|
+
|
|
57
|
+
const tags = [
|
|
58
|
+
// Basic OpenGraph
|
|
59
|
+
og.title && `<meta property="og:title" content="${this._escape(og.title)}">`,
|
|
60
|
+
og.description && `<meta property="og:description" content="${this._escape(og.description)}">`,
|
|
61
|
+
image && `<meta property="og:image" content="${this._escape(image)}">`,
|
|
62
|
+
og.url && `<meta property="og:url" content="${this._escape(og.url)}">`,
|
|
63
|
+
`<meta property="og:type" content="${this._escape(og.type)}">`,
|
|
64
|
+
`<meta property="og:site_name" content="${this._escape(og.siteName)}">`,
|
|
65
|
+
`<meta property="og:locale" content="${this._escape(og.locale)}">`,
|
|
66
|
+
|
|
67
|
+
// Image metadata
|
|
68
|
+
og.imageAlt && `<meta property="og:image:alt" content="${this._escape(og.imageAlt)}">`,
|
|
69
|
+
og.imageWidth && `<meta property="og:image:width" content="${og.imageWidth}">`,
|
|
70
|
+
og.imageHeight && `<meta property="og:image:height" content="${og.imageHeight}">`,
|
|
71
|
+
|
|
72
|
+
// Twitter Cards
|
|
73
|
+
`<meta name="twitter:card" content="${this._escape(og.twitterCard)}">`,
|
|
74
|
+
og.twitterSite && `<meta name="twitter:site" content="${this._escape(og.twitterSite)}">`,
|
|
75
|
+
og.twitterCreator && `<meta name="twitter:creator" content="${this._escape(og.twitterCreator)}">`,
|
|
76
|
+
og.title && `<meta name="twitter:title" content="${this._escape(og.title)}">`,
|
|
77
|
+
og.description && `<meta name="twitter:description" content="${this._escape(og.description)}">`,
|
|
78
|
+
image && `<meta name="twitter:image" content="${this._escape(image)}">`,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
return tags.filter(Boolean).join('\n ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create Hono middleware that injects OG helper into context
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* app.use('*', ogHelper.middleware());
|
|
89
|
+
*
|
|
90
|
+
* // In route handler:
|
|
91
|
+
* const ogTags = c.get('og')({ title: 'My Page', ... });
|
|
92
|
+
*/
|
|
93
|
+
middleware() {
|
|
94
|
+
return async (c, next) => {
|
|
95
|
+
c.set('og', (data) => this.generateTags(data));
|
|
96
|
+
await next();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Escape HTML entities to prevent XSS
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
_escape(str) {
|
|
105
|
+
if (str === null || str === undefined) return '';
|
|
106
|
+
|
|
107
|
+
return String(str)
|
|
108
|
+
.replace(/&/g, '&')
|
|
109
|
+
.replace(/</g, '<')
|
|
110
|
+
.replace(/>/g, '>')
|
|
111
|
+
.replace(/"/g, '"')
|
|
112
|
+
.replace(/'/g, ''');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default OpenGraphHelper;
|