s3db.js 11.3.2 → 12.0.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 +102 -8
- package/dist/s3db.cjs.js +36945 -15510
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +66 -1
- package/dist/s3db.es.js +36914 -15534
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +35 -15
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +79 -49
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +97 -47
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +544 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +354 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicator.plugin.js +2 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +315 -36
- package/src/s3db.d.ts +66 -1
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
package/src/database.class.js
CHANGED
|
@@ -88,11 +88,8 @@ export class Database extends EventEmitter {
|
|
|
88
88
|
if (typeof process !== 'undefined') {
|
|
89
89
|
process.on('exit', async () => {
|
|
90
90
|
if (this.isConnected()) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch (err) {
|
|
94
|
-
// Silently ignore errors on exit
|
|
95
|
-
}
|
|
91
|
+
// Silently ignore errors on exit
|
|
92
|
+
await tryFn(() => this.disconnect());
|
|
96
93
|
}
|
|
97
94
|
});
|
|
98
95
|
}
|
|
@@ -107,26 +104,28 @@ export class Database extends EventEmitter {
|
|
|
107
104
|
let healingLog = [];
|
|
108
105
|
|
|
109
106
|
if (await this.client.exists(`s3db.json`)) {
|
|
110
|
-
|
|
107
|
+
const [ok, error] = await tryFn(async () => {
|
|
111
108
|
const request = await this.client.getObject(`s3db.json`);
|
|
112
109
|
const rawContent = await streamToString(request?.Body);
|
|
113
|
-
|
|
110
|
+
|
|
114
111
|
// Try to parse JSON
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
const [parseOk, parseError, parsedData] = tryFn(() => JSON.parse(rawContent));
|
|
113
|
+
|
|
114
|
+
if (!parseOk) {
|
|
118
115
|
healingLog.push('JSON parsing failed - attempting recovery');
|
|
119
116
|
needsHealing = true;
|
|
120
|
-
|
|
117
|
+
|
|
121
118
|
// Attempt to fix common JSON issues
|
|
122
119
|
metadata = await this._attemptJsonRecovery(rawContent, healingLog);
|
|
123
|
-
|
|
120
|
+
|
|
124
121
|
if (!metadata) {
|
|
125
122
|
// Create backup and start fresh
|
|
126
123
|
await this._createCorruptedBackup(rawContent);
|
|
127
124
|
healingLog.push('Created backup of corrupted file - starting with blank metadata');
|
|
128
125
|
metadata = this.blankMetadataStructure();
|
|
129
126
|
}
|
|
127
|
+
} else {
|
|
128
|
+
metadata = parsedData;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
// Validate and heal metadata structure
|
|
@@ -135,8 +134,9 @@ export class Database extends EventEmitter {
|
|
|
135
134
|
metadata = healedMetadata;
|
|
136
135
|
needsHealing = true;
|
|
137
136
|
}
|
|
137
|
+
});
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
if (!ok) {
|
|
140
140
|
healingLog.push(`Critical error reading s3db.json: ${error.message}`);
|
|
141
141
|
await this._createCorruptedBackup();
|
|
142
142
|
metadata = this.blankMetadataStructure();
|
|
@@ -159,7 +159,7 @@ export class Database extends EventEmitter {
|
|
|
159
159
|
|
|
160
160
|
// Create resources from saved metadata using current version
|
|
161
161
|
for (const [name, resourceMetadata] of Object.entries(metadata.resources || {})) {
|
|
162
|
-
const currentVersion = resourceMetadata.currentVersion || '
|
|
162
|
+
const currentVersion = resourceMetadata.currentVersion || 'v1';
|
|
163
163
|
const versionData = resourceMetadata.versions?.[currentVersion];
|
|
164
164
|
|
|
165
165
|
if (versionData) {
|
|
@@ -240,7 +240,7 @@ export class Database extends EventEmitter {
|
|
|
240
240
|
});
|
|
241
241
|
} else {
|
|
242
242
|
// Get current version hash from saved metadata
|
|
243
|
-
const currentVersion = savedResource.currentVersion || '
|
|
243
|
+
const currentVersion = savedResource.currentVersion || 'v1';
|
|
244
244
|
const versionData = savedResource.versions?.[currentVersion];
|
|
245
245
|
const savedHash = versionData?.hash;
|
|
246
246
|
|
|
@@ -260,7 +260,7 @@ export class Database extends EventEmitter {
|
|
|
260
260
|
// Check for deleted resources
|
|
261
261
|
for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
|
|
262
262
|
if (!this.resources[name]) {
|
|
263
|
-
const currentVersion = savedResource.currentVersion || '
|
|
263
|
+
const currentVersion = savedResource.currentVersion || 'v1';
|
|
264
264
|
const versionData = savedResource.versions?.[currentVersion];
|
|
265
265
|
changes.push({
|
|
266
266
|
type: 'deleted',
|
|
@@ -312,8 +312,8 @@ export class Database extends EventEmitter {
|
|
|
312
312
|
.filter(v => v.startsWith('v'))
|
|
313
313
|
.map(v => parseInt(v.substring(1)))
|
|
314
314
|
.filter(n => !isNaN(n));
|
|
315
|
-
|
|
316
|
-
const maxVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) :
|
|
315
|
+
|
|
316
|
+
const maxVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) : 0;
|
|
317
317
|
return `v${maxVersion + 1}`;
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -325,24 +325,25 @@ export class Database extends EventEmitter {
|
|
|
325
325
|
*/
|
|
326
326
|
_serializeHooks(hooks) {
|
|
327
327
|
if (!hooks || typeof hooks !== 'object') return hooks;
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
const serialized = {};
|
|
330
330
|
for (const [event, hookArray] of Object.entries(hooks)) {
|
|
331
331
|
if (Array.isArray(hookArray)) {
|
|
332
332
|
serialized[event] = hookArray.map(hook => {
|
|
333
333
|
if (typeof hook === 'function') {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
const [ok, err, data] = tryFn(() => ({
|
|
335
|
+
__s3db_serialized_function: true,
|
|
336
|
+
code: hook.toString(),
|
|
337
|
+
name: hook.name || 'anonymous'
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
if (!ok) {
|
|
341
341
|
if (this.verbose) {
|
|
342
342
|
console.warn(`Failed to serialize hook for event '${event}':`, err.message);
|
|
343
343
|
}
|
|
344
344
|
return null;
|
|
345
345
|
}
|
|
346
|
+
return data;
|
|
346
347
|
}
|
|
347
348
|
return hook;
|
|
348
349
|
});
|
|
@@ -361,24 +362,25 @@ export class Database extends EventEmitter {
|
|
|
361
362
|
*/
|
|
362
363
|
_deserializeHooks(serializedHooks) {
|
|
363
364
|
if (!serializedHooks || typeof serializedHooks !== 'object') return serializedHooks;
|
|
364
|
-
|
|
365
|
+
|
|
365
366
|
const deserialized = {};
|
|
366
367
|
for (const [event, hookArray] of Object.entries(serializedHooks)) {
|
|
367
368
|
if (Array.isArray(hookArray)) {
|
|
368
369
|
deserialized[event] = hookArray.map(hook => {
|
|
369
370
|
if (hook && typeof hook === 'object' && hook.__s3db_serialized_function) {
|
|
370
|
-
|
|
371
|
+
const [ok, err, fn] = tryFn(() => {
|
|
371
372
|
// Use Function constructor instead of eval for better security
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
373
|
+
const func = new Function('return ' + hook.code)();
|
|
374
|
+
return typeof func === 'function' ? func : null;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (!ok || fn === null) {
|
|
377
378
|
if (this.verbose) {
|
|
378
|
-
console.warn(`Failed to deserialize hook '${hook.name}' for event '${event}':`, err
|
|
379
|
+
console.warn(`Failed to deserialize hook '${hook.name}' for event '${event}':`, err?.message || 'Invalid function');
|
|
379
380
|
}
|
|
381
|
+
return null;
|
|
380
382
|
}
|
|
381
|
-
return
|
|
383
|
+
return fn;
|
|
382
384
|
}
|
|
383
385
|
return hook;
|
|
384
386
|
}).filter(hook => hook !== null); // Remove failed deserializations
|
|
@@ -498,7 +500,7 @@ export class Database extends EventEmitter {
|
|
|
498
500
|
|
|
499
501
|
// Check if resource exists in saved metadata
|
|
500
502
|
const existingResource = this.savedMetadata?.resources?.[name];
|
|
501
|
-
const currentVersion = existingResource?.currentVersion || '
|
|
503
|
+
const currentVersion = existingResource?.currentVersion || 'v1';
|
|
502
504
|
const existingVersionData = existingResource?.versions?.[currentVersion];
|
|
503
505
|
|
|
504
506
|
let version, isNewVersion;
|
|
@@ -626,14 +628,16 @@ export class Database extends EventEmitter {
|
|
|
626
628
|
];
|
|
627
629
|
|
|
628
630
|
for (const [index, fix] of fixes.entries()) {
|
|
629
|
-
|
|
631
|
+
const [ok, err, parsed] = tryFn(() => {
|
|
630
632
|
const fixedContent = fix();
|
|
631
|
-
|
|
633
|
+
return JSON.parse(fixedContent);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (ok) {
|
|
632
637
|
healingLog.push(`JSON recovery successful using fix #${index + 1}`);
|
|
633
638
|
return parsed;
|
|
634
|
-
} catch (error) {
|
|
635
|
-
// Try next fix
|
|
636
639
|
}
|
|
640
|
+
// Try next fix
|
|
637
641
|
}
|
|
638
642
|
|
|
639
643
|
healingLog.push('All JSON recovery attempts failed');
|
|
@@ -723,7 +727,7 @@ export class Database extends EventEmitter {
|
|
|
723
727
|
|
|
724
728
|
// Ensure currentVersion exists
|
|
725
729
|
if (!healed.currentVersion) {
|
|
726
|
-
healed.currentVersion = '
|
|
730
|
+
healed.currentVersion = 'v1';
|
|
727
731
|
healingLog.push(`Resource ${name}: added missing currentVersion`);
|
|
728
732
|
changed = true;
|
|
729
733
|
}
|
|
@@ -822,17 +826,16 @@ export class Database extends EventEmitter {
|
|
|
822
826
|
* Create backup of corrupted file
|
|
823
827
|
*/
|
|
824
828
|
async _createCorruptedBackup(content = null) {
|
|
825
|
-
|
|
829
|
+
const [ok, err] = await tryFn(async () => {
|
|
826
830
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
827
831
|
const backupKey = `s3db.json.corrupted.${timestamp}.backup`;
|
|
828
|
-
|
|
832
|
+
|
|
829
833
|
if (!content) {
|
|
830
|
-
|
|
834
|
+
const [readOk, readErr, readData] = await tryFn(async () => {
|
|
831
835
|
const request = await this.client.getObject(`s3db.json`);
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
+
return await streamToString(request?.Body);
|
|
837
|
+
});
|
|
838
|
+
content = readOk ? readData : 'Unable to read corrupted file content';
|
|
836
839
|
}
|
|
837
840
|
|
|
838
841
|
await this.client.putObject({
|
|
@@ -844,10 +847,10 @@ export class Database extends EventEmitter {
|
|
|
844
847
|
if (this.verbose) {
|
|
845
848
|
console.warn(`S3DB: Created backup of corrupted s3db.json as ${backupKey}`);
|
|
846
849
|
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
if (!ok && this.verbose) {
|
|
853
|
+
console.warn(`S3DB: Failed to create backup: ${err.message}`);
|
|
851
854
|
}
|
|
852
855
|
}
|
|
853
856
|
|
|
@@ -855,7 +858,7 @@ export class Database extends EventEmitter {
|
|
|
855
858
|
* Upload healed metadata with logging
|
|
856
859
|
*/
|
|
857
860
|
async _uploadHealedMetadata(metadata, healingLog) {
|
|
858
|
-
|
|
861
|
+
const [ok, err] = await tryFn(async () => {
|
|
859
862
|
if (this.verbose && healingLog.length > 0) {
|
|
860
863
|
console.warn('S3DB Self-Healing Operations:');
|
|
861
864
|
healingLog.forEach(log => console.warn(` - ${log}`));
|
|
@@ -875,11 +878,13 @@ export class Database extends EventEmitter {
|
|
|
875
878
|
if (this.verbose) {
|
|
876
879
|
console.warn('S3DB: Successfully uploaded healed metadata');
|
|
877
880
|
}
|
|
878
|
-
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (!ok) {
|
|
879
884
|
if (this.verbose) {
|
|
880
|
-
console.error(`S3DB: Failed to upload healed metadata: ${
|
|
885
|
+
console.error(`S3DB: Failed to upload healed metadata: ${err.message}`);
|
|
881
886
|
}
|
|
882
|
-
throw
|
|
887
|
+
throw err;
|
|
883
888
|
}
|
|
884
889
|
}
|
|
885
890
|
|
|
@@ -979,7 +984,7 @@ export class Database extends EventEmitter {
|
|
|
979
984
|
// Only upload metadata if hash actually changed
|
|
980
985
|
const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
|
|
981
986
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
982
|
-
const currentVersion = existingMetadata?.currentVersion || '
|
|
987
|
+
const currentVersion = existingMetadata?.currentVersion || 'v1';
|
|
983
988
|
const existingVersionData = existingMetadata?.versions?.[currentVersion];
|
|
984
989
|
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
985
990
|
await this.uploadMetadataFile();
|
|
@@ -988,7 +993,7 @@ export class Database extends EventEmitter {
|
|
|
988
993
|
return existingResource;
|
|
989
994
|
}
|
|
990
995
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
991
|
-
const version = existingMetadata?.currentVersion || '
|
|
996
|
+
const version = existingMetadata?.currentVersion || 'v1';
|
|
992
997
|
const resource = new Resource({
|
|
993
998
|
name,
|
|
994
999
|
client: this.client,
|
|
@@ -1011,6 +1016,7 @@ export class Database extends EventEmitter {
|
|
|
1011
1016
|
idGenerator: config.idGenerator,
|
|
1012
1017
|
idSize: config.idSize,
|
|
1013
1018
|
asyncEvents: config.asyncEvents,
|
|
1019
|
+
asyncPartitions: config.asyncPartitions !== undefined ? config.asyncPartitions : true,
|
|
1014
1020
|
events: config.events || {},
|
|
1015
1021
|
createdBy: config.createdBy || 'user'
|
|
1016
1022
|
});
|
|
@@ -1073,7 +1079,8 @@ export class Database extends EventEmitter {
|
|
|
1073
1079
|
}
|
|
1074
1080
|
|
|
1075
1081
|
async disconnect() {
|
|
1076
|
-
|
|
1082
|
+
// Silently ignore all errors during disconnect
|
|
1083
|
+
await tryFn(async () => {
|
|
1077
1084
|
// 1. Remove all listeners from all plugins
|
|
1078
1085
|
if (this.pluginList && this.pluginList.length > 0) {
|
|
1079
1086
|
for (const plugin of this.pluginList) {
|
|
@@ -1083,13 +1090,12 @@ export class Database extends EventEmitter {
|
|
|
1083
1090
|
}
|
|
1084
1091
|
// Also stop plugins if they have a stop method
|
|
1085
1092
|
const stopProms = this.pluginList.map(async (plugin) => {
|
|
1086
|
-
|
|
1093
|
+
// Silently ignore errors on exit
|
|
1094
|
+
await tryFn(async () => {
|
|
1087
1095
|
if (plugin && typeof plugin.stop === 'function') {
|
|
1088
1096
|
await plugin.stop();
|
|
1089
1097
|
}
|
|
1090
|
-
}
|
|
1091
|
-
// Silently ignore errors on exit
|
|
1092
|
-
}
|
|
1098
|
+
});
|
|
1093
1099
|
});
|
|
1094
1100
|
await Promise.all(stopProms);
|
|
1095
1101
|
}
|
|
@@ -1097,7 +1103,8 @@ export class Database extends EventEmitter {
|
|
|
1097
1103
|
// 2. Remove all listeners from all resources
|
|
1098
1104
|
if (this.resources && Object.keys(this.resources).length > 0) {
|
|
1099
1105
|
for (const [name, resource] of Object.entries(this.resources)) {
|
|
1100
|
-
|
|
1106
|
+
// Silently ignore errors on exit
|
|
1107
|
+
await tryFn(() => {
|
|
1101
1108
|
if (resource && typeof resource.removeAllListeners === 'function') {
|
|
1102
1109
|
resource.removeAllListeners();
|
|
1103
1110
|
}
|
|
@@ -1110,9 +1117,7 @@ export class Database extends EventEmitter {
|
|
|
1110
1117
|
if (resource.observers && Array.isArray(resource.observers)) {
|
|
1111
1118
|
resource.observers = [];
|
|
1112
1119
|
}
|
|
1113
|
-
}
|
|
1114
|
-
// Silently ignore errors on exit
|
|
1115
|
-
}
|
|
1120
|
+
});
|
|
1116
1121
|
}
|
|
1117
1122
|
// Instead of reassigning, clear in place
|
|
1118
1123
|
Object.keys(this.resources).forEach(k => delete this.resources[k]);
|
|
@@ -1132,9 +1137,7 @@ export class Database extends EventEmitter {
|
|
|
1132
1137
|
this.pluginList = [];
|
|
1133
1138
|
|
|
1134
1139
|
this.emit('disconnected', new Date());
|
|
1135
|
-
}
|
|
1136
|
-
// Silently ignore errors on exit
|
|
1137
|
-
}
|
|
1140
|
+
});
|
|
1138
1141
|
}
|
|
1139
1142
|
|
|
1140
1143
|
/**
|
|
@@ -1249,12 +1252,11 @@ export class Database extends EventEmitter {
|
|
|
1249
1252
|
*/
|
|
1250
1253
|
async _executeHooks(event, context = {}) {
|
|
1251
1254
|
if (!this._hooks || !this._hooks.has(event)) return;
|
|
1252
|
-
|
|
1255
|
+
|
|
1253
1256
|
const hooks = this._hooks.get(event);
|
|
1254
1257
|
for (const hook of hooks) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
} catch (error) {
|
|
1258
|
+
const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
|
|
1259
|
+
if (!ok) {
|
|
1258
1260
|
// Emit error but don't stop hook execution
|
|
1259
1261
|
this.emit('hookError', { event, error, context });
|
|
1260
1262
|
}
|
package/src/errors.js
CHANGED
|
@@ -498,8 +498,6 @@ Solution:
|
|
|
498
498
|
${queueSize >= maxQueueSize
|
|
499
499
|
? 'Wait for queue to drain or increase maxQueueSize'
|
|
500
500
|
: 'Check driver configuration and permissions'}
|
|
501
|
-
|
|
502
|
-
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
503
501
|
`.trim();
|
|
504
502
|
} else if (!description) {
|
|
505
503
|
description = `
|
|
@@ -509,8 +507,6 @@ Driver: ${driver}
|
|
|
509
507
|
Operation: ${operation}
|
|
510
508
|
|
|
511
509
|
Check driver configuration and permissions.
|
|
512
|
-
|
|
513
|
-
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
514
510
|
`.trim();
|
|
515
511
|
}
|
|
516
512
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Authentication - Simple API key authentication middleware
|
|
3
|
+
*
|
|
4
|
+
* Provides authentication using static API keys in headers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { unauthorized } from '../utils/response-formatter.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate random API key
|
|
11
|
+
* @param {number} length - Key length (default: 32)
|
|
12
|
+
* @returns {string} Random API key
|
|
13
|
+
*/
|
|
14
|
+
export function generateApiKey(length = 32) {
|
|
15
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
16
|
+
let apiKey = '';
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < length; i++) {
|
|
19
|
+
apiKey += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return apiKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create API Key authentication middleware
|
|
27
|
+
* @param {Object} options - API Key options
|
|
28
|
+
* @param {string} options.headerName - Header name for API key (default: 'X-API-Key')
|
|
29
|
+
* @param {Object} options.usersResource - Users resource for key validation
|
|
30
|
+
* @param {boolean} options.optional - If true, allows requests without auth
|
|
31
|
+
* @returns {Function} Hono middleware
|
|
32
|
+
*/
|
|
33
|
+
export function apiKeyAuth(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
headerName = 'X-API-Key',
|
|
36
|
+
usersResource,
|
|
37
|
+
optional = false
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
if (!usersResource) {
|
|
41
|
+
throw new Error('usersResource is required for API key authentication');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return async (c, next) => {
|
|
45
|
+
const apiKey = c.req.header(headerName);
|
|
46
|
+
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
if (optional) {
|
|
49
|
+
return await next();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const response = unauthorized(`Missing ${headerName} header`);
|
|
53
|
+
return c.json(response, response._status);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Query users by API key
|
|
57
|
+
try {
|
|
58
|
+
const users = await usersResource.query({ apiKey });
|
|
59
|
+
|
|
60
|
+
if (!users || users.length === 0) {
|
|
61
|
+
const response = unauthorized('Invalid API key');
|
|
62
|
+
return c.json(response, response._status);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const user = users[0];
|
|
66
|
+
|
|
67
|
+
if (!user.active) {
|
|
68
|
+
const response = unauthorized('User account is inactive');
|
|
69
|
+
return c.json(response, response._status);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Store user in context
|
|
73
|
+
c.set('user', user);
|
|
74
|
+
c.set('authMethod', 'apiKey');
|
|
75
|
+
|
|
76
|
+
await next();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[API Key Auth] Error validating key:', err);
|
|
79
|
+
const response = unauthorized('Authentication error');
|
|
80
|
+
return c.json(response, response._status);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default {
|
|
86
|
+
generateApiKey,
|
|
87
|
+
apiKeyAuth
|
|
88
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic Authentication - HTTP Basic Auth middleware
|
|
3
|
+
*
|
|
4
|
+
* Provides authentication using username:password in Authorization header
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { unauthorized } from '../utils/response-formatter.js';
|
|
8
|
+
import { decrypt } from '../../../concerns/crypto.js';
|
|
9
|
+
import tryFn from '../../../concerns/try-fn.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse Basic Auth header
|
|
13
|
+
* @param {string} authHeader - Authorization header value
|
|
14
|
+
* @returns {Object|null} { username, password } or null if invalid
|
|
15
|
+
*/
|
|
16
|
+
export function parseBasicAuth(authHeader) {
|
|
17
|
+
if (!authHeader) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const match = authHeader.match(/^Basic\s+(.+)$/i);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const decoded = Buffer.from(match[1], 'base64').toString('utf-8');
|
|
28
|
+
const [username, ...passwordParts] = decoded.split(':');
|
|
29
|
+
const password = passwordParts.join(':'); // Handle passwords with colons
|
|
30
|
+
|
|
31
|
+
if (!username || !password) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { username, password };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Verify password against stored hash
|
|
43
|
+
* @param {string} inputPassword - Plain text password from request
|
|
44
|
+
* @param {string} storedPassword - Encrypted password from database
|
|
45
|
+
* @param {string} passphrase - Encryption passphrase
|
|
46
|
+
* @returns {Promise<boolean>} True if password matches
|
|
47
|
+
*/
|
|
48
|
+
async function verifyPassword(inputPassword, storedPassword, passphrase) {
|
|
49
|
+
try {
|
|
50
|
+
// Decrypt stored password
|
|
51
|
+
const [ok, err, decrypted] = await tryFn(() =>
|
|
52
|
+
decrypt(storedPassword, passphrase)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!ok) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Compare
|
|
60
|
+
return decrypted === inputPassword;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create Basic Auth middleware
|
|
68
|
+
* @param {Object} options - Basic Auth options
|
|
69
|
+
* @param {string} options.realm - Authentication realm (default: 'API Access')
|
|
70
|
+
* @param {Object} options.usersResource - Users resource for credential validation
|
|
71
|
+
* @param {string} options.passphrase - Passphrase for password decryption
|
|
72
|
+
* @param {boolean} options.optional - If true, allows requests without auth
|
|
73
|
+
* @returns {Function} Hono middleware
|
|
74
|
+
*/
|
|
75
|
+
export function basicAuth(options = {}) {
|
|
76
|
+
const {
|
|
77
|
+
realm = 'API Access',
|
|
78
|
+
usersResource,
|
|
79
|
+
passphrase = 'secret',
|
|
80
|
+
optional = false
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
if (!usersResource) {
|
|
84
|
+
throw new Error('usersResource is required for Basic authentication');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return async (c, next) => {
|
|
88
|
+
const authHeader = c.req.header('authorization');
|
|
89
|
+
|
|
90
|
+
if (!authHeader) {
|
|
91
|
+
if (optional) {
|
|
92
|
+
return await next();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
96
|
+
const response = unauthorized('Basic authentication required');
|
|
97
|
+
return c.json(response, response._status);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const credentials = parseBasicAuth(authHeader);
|
|
101
|
+
|
|
102
|
+
if (!credentials) {
|
|
103
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
104
|
+
const response = unauthorized('Invalid Basic authentication format');
|
|
105
|
+
return c.json(response, response._status);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { username, password } = credentials;
|
|
109
|
+
|
|
110
|
+
// Query user by username
|
|
111
|
+
try {
|
|
112
|
+
const users = await usersResource.query({ username });
|
|
113
|
+
|
|
114
|
+
if (!users || users.length === 0) {
|
|
115
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
116
|
+
const response = unauthorized('Invalid credentials');
|
|
117
|
+
return c.json(response, response._status);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const user = users[0];
|
|
121
|
+
|
|
122
|
+
if (!user.active) {
|
|
123
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
124
|
+
const response = unauthorized('User account is inactive');
|
|
125
|
+
return c.json(response, response._status);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Verify password
|
|
129
|
+
const isValid = await verifyPassword(password, user.password, passphrase);
|
|
130
|
+
|
|
131
|
+
if (!isValid) {
|
|
132
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
133
|
+
const response = unauthorized('Invalid credentials');
|
|
134
|
+
return c.json(response, response._status);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Store user in context
|
|
138
|
+
c.set('user', user);
|
|
139
|
+
c.set('authMethod', 'basic');
|
|
140
|
+
|
|
141
|
+
await next();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('[Basic Auth] Error validating credentials:', err);
|
|
144
|
+
c.header('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
145
|
+
const response = unauthorized('Authentication error');
|
|
146
|
+
return c.json(response, response._status);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default {
|
|
152
|
+
parseBasicAuth,
|
|
153
|
+
basicAuth
|
|
154
|
+
};
|