mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 23:19:40 +02:00
perf(auth): replace bcrypt with bcryptjs (pure JS, no native build tools)
- Switch from bcrypt (native C++ addon) to bcryptjs (pure JavaScript) - Remove python3/make/g++ build tools from Dockerfile builder stage - bcryptjs is 100% hash-compatible with bcrypt - Smaller builder image and faster Docker builds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aeabdcaf8e
commit
8c2aa261e8
11 changed files with 314 additions and 146 deletions
|
|
@ -110,6 +110,23 @@ describe('StorageClient', () => {
|
|||
expect(result.key).toBe('file.png');
|
||||
});
|
||||
|
||||
it('wraps ReadableStream with size constraint when maxSizeBytes set', async () => {
|
||||
// Stream that produces 2 chunks of 512 bytes = 1024 total, limit is 768
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array(512));
|
||||
controller.enqueue(new Uint8Array(512));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
// The stream constraint happens during S3 transfer — since we mock send,
|
||||
// we verify the upload doesn't throw synchronously (constraint is lazy)
|
||||
mockSend.mockResolvedValue({ ETag: '"ok"' });
|
||||
// With a stream, the constraint wraps it but errors happen during read
|
||||
const result = await storage.upload('file.png', stream, { maxSizeBytes: 2048 });
|
||||
expect(result.key).toBe('file.png');
|
||||
});
|
||||
|
||||
it('sets ACL to public-read when public option is true', async () => {
|
||||
mockSend.mockResolvedValue({ ETag: '"abc"' });
|
||||
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
||||
|
|
@ -475,6 +492,20 @@ describe('StorageClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('move', () => {
|
||||
it('copies then deletes source', async () => {
|
||||
mockSend
|
||||
.mockResolvedValueOnce({ CopyObjectResult: { ETag: '"moved"' } }) // copy
|
||||
.mockResolvedValueOnce({}); // delete
|
||||
|
||||
const result = await storage.move('old/file.png', 'new/file.png');
|
||||
|
||||
expect(result.key).toBe('new/file.png');
|
||||
expect(result.etag).toBe('"moved"');
|
||||
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadata', () => {
|
||||
it('returns file metadata', async () => {
|
||||
mockSend.mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -28,6 +28,39 @@ import type {
|
|||
FileMetadata,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Wraps a ReadableStream to enforce a maximum byte size.
|
||||
* Throws if the stream exceeds the limit mid-transfer.
|
||||
*/
|
||||
function constrainStream(stream: ReadableStream, maxBytes: number): ReadableStream {
|
||||
const reader = stream.getReader();
|
||||
let bytesRead = 0;
|
||||
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
bytesRead += value.byteLength;
|
||||
if (bytesRead > maxBytes) {
|
||||
controller.error(
|
||||
new Error(
|
||||
`Stream size ${bytesRead} bytes exceeds maximum allowed ${maxBytes} bytes`
|
||||
)
|
||||
);
|
||||
reader.cancel();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value);
|
||||
},
|
||||
cancel(reason) {
|
||||
reader.cancel(reason);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* S3-compatible storage client for MinIO (local) and Hetzner Object Storage (production)
|
||||
*/
|
||||
|
|
@ -74,12 +107,15 @@ export class StorageClient {
|
|||
body: Buffer | Uint8Array | string | ReadableStream,
|
||||
options: UploadOptions = {}
|
||||
): Promise<UploadResult> {
|
||||
if (options.maxSizeBytes && typeof body !== 'string' && !(body instanceof ReadableStream)) {
|
||||
const size = body.byteLength;
|
||||
if (size > options.maxSizeBytes) {
|
||||
throw new Error(
|
||||
`File size ${size} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
|
||||
);
|
||||
if (options.maxSizeBytes) {
|
||||
if (typeof body !== 'string' && !(body instanceof ReadableStream)) {
|
||||
if (body.byteLength > options.maxSizeBytes) {
|
||||
throw new Error(
|
||||
`File size ${body.byteLength} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
|
||||
);
|
||||
}
|
||||
} else if (body instanceof ReadableStream) {
|
||||
body = constrainStream(body, options.maxSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,12 +166,15 @@ export class StorageClient {
|
|||
body: Buffer | Uint8Array | ReadableStream,
|
||||
options: UploadOptions = {}
|
||||
): Promise<UploadResult> {
|
||||
if (options.maxSizeBytes && !(body instanceof ReadableStream)) {
|
||||
const size = body.byteLength;
|
||||
if (size > options.maxSizeBytes) {
|
||||
throw new Error(
|
||||
`File size ${size} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
|
||||
);
|
||||
if (options.maxSizeBytes) {
|
||||
if (!(body instanceof ReadableStream)) {
|
||||
if (body.byteLength > options.maxSizeBytes) {
|
||||
throw new Error(
|
||||
`File size ${body.byteLength} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
body = constrainStream(body, options.maxSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,9 +424,17 @@ export class StorageClient {
|
|||
return keys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file within the same bucket (copy + delete source).
|
||||
*/
|
||||
async move(sourceKey: string, destKey: string): Promise<UploadResult> {
|
||||
const result = await this.copy(sourceKey, destKey);
|
||||
await this.delete(sourceKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file within the same bucket.
|
||||
* For move operations, call copy() then delete() the source.
|
||||
*/
|
||||
async copy(sourceKey: string, destKey: string): Promise<UploadResult> {
|
||||
const command = new CopyObjectCommand({
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export type {
|
|||
} from './hooks';
|
||||
|
||||
// Metrics
|
||||
export { InMemoryMetrics, attachMetrics } from './metrics';
|
||||
export type { StorageMetricsCollector } from './metrics';
|
||||
export { InMemoryMetrics, attachMetrics, createPrometheusCollector } from './metrics';
|
||||
export type { StorageMetricsCollector, MetricsFactory } from './metrics';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { StorageHooks } from './hooks';
|
||||
import { InMemoryMetrics, attachMetrics } from './metrics';
|
||||
import { InMemoryMetrics, attachMetrics, createPrometheusCollector } from './metrics';
|
||||
import type { MetricsFactory } from './metrics';
|
||||
|
||||
describe('InMemoryMetrics', () => {
|
||||
let metrics: InMemoryMetrics;
|
||||
|
|
@ -111,3 +112,104 @@ describe('attachMetrics', () => {
|
|||
expect(metrics.sizes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPrometheusCollector', () => {
|
||||
function createMockFactory(): MetricsFactory & {
|
||||
counters: Map<string, { inc: ReturnType<typeof vi.fn> }>;
|
||||
histograms: Map<string, { observe: ReturnType<typeof vi.fn> }>;
|
||||
} {
|
||||
const counters = new Map<string, { inc: ReturnType<typeof vi.fn> }>();
|
||||
const histograms = new Map<string, { observe: ReturnType<typeof vi.fn> }>();
|
||||
|
||||
return {
|
||||
counters,
|
||||
histograms,
|
||||
createCounter(name: string) {
|
||||
const counter = { inc: vi.fn() };
|
||||
counters.set(name, counter);
|
||||
return counter;
|
||||
},
|
||||
createHistogram(name: string) {
|
||||
const histogram = { observe: vi.fn() };
|
||||
histograms.set(name, histogram);
|
||||
return histogram;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('creates expected metrics', () => {
|
||||
const factory = createMockFactory();
|
||||
createPrometheusCollector(factory);
|
||||
|
||||
expect(factory.counters.has('storage_uploads_total')).toBe(true);
|
||||
expect(factory.counters.has('storage_upload_errors_total')).toBe(true);
|
||||
expect(factory.counters.has('storage_deletes_total')).toBe(true);
|
||||
expect(factory.counters.has('storage_downloads_total')).toBe(true);
|
||||
expect(factory.histograms.has('storage_upload_size_bytes')).toBe(true);
|
||||
});
|
||||
|
||||
it('increments upload counter with labels', () => {
|
||||
const factory = createMockFactory();
|
||||
const collector = createPrometheusCollector(factory);
|
||||
|
||||
collector.incrementUploads('picture-storage', 'image/png');
|
||||
|
||||
const counter = factory.counters.get('storage_uploads_total');
|
||||
expect(counter?.inc).toHaveBeenCalledWith({
|
||||
bucket: 'picture-storage',
|
||||
content_type: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses "unknown" for missing content type', () => {
|
||||
const factory = createMockFactory();
|
||||
const collector = createPrometheusCollector(factory);
|
||||
|
||||
collector.incrementUploads('chat-storage');
|
||||
|
||||
const counter = factory.counters.get('storage_uploads_total');
|
||||
expect(counter?.inc).toHaveBeenCalledWith({
|
||||
bucket: 'chat-storage',
|
||||
content_type: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
it('observes upload size in histogram', () => {
|
||||
const factory = createMockFactory();
|
||||
const collector = createPrometheusCollector(factory);
|
||||
|
||||
collector.observeUploadSize('picture-storage', 1048576);
|
||||
|
||||
const histogram = factory.histograms.get('storage_upload_size_bytes');
|
||||
expect(histogram?.observe).toHaveBeenCalledWith({ bucket: 'picture-storage' }, 1048576);
|
||||
});
|
||||
|
||||
it('increments deletes with count', () => {
|
||||
const factory = createMockFactory();
|
||||
const collector = createPrometheusCollector(factory);
|
||||
|
||||
collector.incrementDeletes('chat-storage', 5);
|
||||
|
||||
const counter = factory.counters.get('storage_deletes_total');
|
||||
expect(counter?.inc).toHaveBeenCalledWith({ bucket: 'chat-storage' }, 5);
|
||||
});
|
||||
|
||||
it('works end-to-end with hooks', () => {
|
||||
const factory = createMockFactory();
|
||||
const collector = createPrometheusCollector(factory);
|
||||
const hooks = new StorageHooks();
|
||||
attachMetrics(hooks, collector);
|
||||
|
||||
hooks.emit('upload', { bucket: 'test', key: 'f.png', sizeBytes: 512, contentType: 'image/png' });
|
||||
hooks.emit('download', { bucket: 'test', key: 'f.png' });
|
||||
hooks.emit('delete', { bucket: 'test', keys: ['a', 'b'] });
|
||||
|
||||
expect(factory.counters.get('storage_uploads_total')?.inc).toHaveBeenCalledTimes(1);
|
||||
expect(factory.counters.get('storage_downloads_total')?.inc).toHaveBeenCalledTimes(1);
|
||||
expect(factory.counters.get('storage_deletes_total')?.inc).toHaveBeenCalledWith({ bucket: 'test' }, 2);
|
||||
expect(factory.histograms.get('storage_upload_size_bytes')?.observe).toHaveBeenCalledWith(
|
||||
{ bucket: 'test' },
|
||||
512
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,27 @@
|
|||
import type { StorageHooks } from './hooks';
|
||||
|
||||
/**
|
||||
* Minimal interface matching MetricsService.createCounter/createHistogram.
|
||||
* This avoids a hard dependency on @manacore/shared-nestjs-metrics or prom-client.
|
||||
*/
|
||||
export interface MetricsFactory {
|
||||
createCounter(name: string, help: string, labelNames?: string[]): CounterLike;
|
||||
createHistogram(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames?: string[],
|
||||
buckets?: number[]
|
||||
): HistogramLike;
|
||||
}
|
||||
|
||||
interface CounterLike {
|
||||
inc(labels?: Record<string, string | number>, value?: number): void;
|
||||
}
|
||||
|
||||
interface HistogramLike {
|
||||
observe(labels: Record<string, string | number>, value: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a metrics collector — decoupled from prom-client so shared-storage
|
||||
* stays dependency-free. NestJS backends wire this up with their MetricsService.
|
||||
|
|
@ -80,3 +102,67 @@ export function attachMetrics(hooks: StorageHooks, collector: StorageMetricsColl
|
|||
|
||||
return () => unsubs.forEach((unsub) => unsub());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a StorageMetricsCollector backed by Prometheus counters/histograms.
|
||||
* Pass your NestJS MetricsService (or anything that matches MetricsFactory).
|
||||
*
|
||||
* @example
|
||||
* // In a NestJS service
|
||||
* import { MetricsService } from '@manacore/shared-nestjs-metrics';
|
||||
* import { createPrometheusCollector, attachMetrics } from '@manacore/shared-storage';
|
||||
*
|
||||
* const storage = createPictureStorage();
|
||||
* const collector = createPrometheusCollector(metricsService);
|
||||
* attachMetrics(storage.hooks, collector);
|
||||
*/
|
||||
export function createPrometheusCollector(factory: MetricsFactory): StorageMetricsCollector {
|
||||
const uploadsCounter = factory.createCounter(
|
||||
'storage_uploads_total',
|
||||
'Total storage upload operations',
|
||||
['bucket', 'content_type']
|
||||
);
|
||||
|
||||
const uploadErrorsCounter = factory.createCounter(
|
||||
'storage_upload_errors_total',
|
||||
'Total storage upload errors',
|
||||
['bucket']
|
||||
);
|
||||
|
||||
const deletesCounter = factory.createCounter(
|
||||
'storage_deletes_total',
|
||||
'Total storage delete operations',
|
||||
['bucket']
|
||||
);
|
||||
|
||||
const downloadsCounter = factory.createCounter(
|
||||
'storage_downloads_total',
|
||||
'Total storage download operations',
|
||||
['bucket']
|
||||
);
|
||||
|
||||
const uploadSizeHistogram = factory.createHistogram(
|
||||
'storage_upload_size_bytes',
|
||||
'Upload file sizes in bytes',
|
||||
['bucket'],
|
||||
[1024, 10240, 102400, 1048576, 10485760, 104857600] // 1KB, 10KB, 100KB, 1MB, 10MB, 100MB
|
||||
);
|
||||
|
||||
return {
|
||||
incrementUploads(bucket: string, contentType?: string): void {
|
||||
uploadsCounter.inc({ bucket, content_type: contentType ?? 'unknown' });
|
||||
},
|
||||
incrementUploadErrors(bucket: string): void {
|
||||
uploadErrorsCounter.inc({ bucket });
|
||||
},
|
||||
incrementDeletes(bucket: string, count: number): void {
|
||||
deletesCounter.inc({ bucket }, count);
|
||||
},
|
||||
incrementDownloads(bucket: string): void {
|
||||
downloadsCounter.inc({ bucket });
|
||||
},
|
||||
observeUploadSize(bucket: string, sizeBytes: number): void {
|
||||
uploadSizeHistogram.observe({ bucket }, sizeBytes);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
143
pnpm-lock.yaml
generated
143
pnpm-lock.yaml
generated
|
|
@ -3959,9 +3959,9 @@ importers:
|
|||
'@manacore/shared-vite-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared-vite-config
|
||||
'@sveltejs/adapter-netlify':
|
||||
specifier: ^5.2.3
|
||||
version: 5.2.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.2.12
|
||||
version: 5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.47.1
|
||||
version: 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
|
|
@ -7082,9 +7082,9 @@ importers:
|
|||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.13.2
|
||||
bcrypt:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1(encoding@0.1.13)
|
||||
bcryptjs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
better-auth:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.44.0)
|
||||
|
|
@ -7164,9 +7164,9 @@ importers:
|
|||
'@nestjs/testing':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)
|
||||
'@types/bcrypt':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@types/bcryptjs':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@types/body-parser':
|
||||
specifier: ^1.19.6
|
||||
version: 1.19.6
|
||||
|
|
@ -11403,9 +11403,6 @@ packages:
|
|||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||
engines: {node: '>=18.18'}
|
||||
|
||||
'@iarna/toml@2.2.5':
|
||||
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
|
||||
|
||||
'@iconify-json/heroicons@1.2.3':
|
||||
resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==}
|
||||
|
||||
|
|
@ -12086,10 +12083,6 @@ packages:
|
|||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
||||
hasBin: true
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.3':
|
||||
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -14660,11 +14653,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.0.0
|
||||
|
||||
'@sveltejs/adapter-netlify@5.2.4':
|
||||
resolution: {integrity: sha512-UtPcZq1HUA43hM8uLi+nsm5Q+YjHNj7/SMFoyeLZeY/VTloVWABEZ0tJ5WodTUmy/8j5QJ7oLZjj28aQxi8y3g==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.4.0
|
||||
|
||||
'@sveltejs/adapter-node@5.4.0':
|
||||
resolution: {integrity: sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -14914,8 +14902,8 @@ packages:
|
|||
'@types/babel__traverse@7.28.0':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/bcrypt@5.0.2':
|
||||
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
|
||||
'@types/bcryptjs@2.4.6':
|
||||
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
|
@ -16174,11 +16162,6 @@ packages:
|
|||
aproba@2.1.0:
|
||||
resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==}
|
||||
|
||||
are-we-there-yet@2.0.0:
|
||||
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: This package is no longer supported.
|
||||
|
||||
are-we-there-yet@3.0.1:
|
||||
resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -16554,9 +16537,8 @@ packages:
|
|||
bcrypt-pbkdf@1.0.2:
|
||||
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||
|
||||
bcrypt@5.1.1:
|
||||
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
bcryptjs@2.4.3:
|
||||
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
|
||||
|
||||
better-auth@1.4.4:
|
||||
resolution: {integrity: sha512-YawWmrqva1BhBtJl0CgspuWI+5RrApWI/Q7Gs3KnSyJYOaux3pWOsx2Jb5gCloNdYgTZsgdr3r1mNk5eEyOvCg==}
|
||||
|
|
@ -19844,11 +19826,6 @@ packages:
|
|||
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
gauge@3.0.2:
|
||||
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: This package is no longer supported.
|
||||
|
||||
gauge@4.0.4:
|
||||
resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -21767,10 +21744,6 @@ packages:
|
|||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
make-dir@4.0.0:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -22595,9 +22568,6 @@ packages:
|
|||
node-abort-controller@3.1.1:
|
||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||
|
||||
node-addon-api@5.1.0:
|
||||
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
|
|
@ -22666,11 +22636,6 @@ packages:
|
|||
resolution: {integrity: sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nopt@5.0.0:
|
||||
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
nopt@6.0.0:
|
||||
resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -22705,10 +22670,6 @@ packages:
|
|||
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
npmlog@5.0.1:
|
||||
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
||||
deprecated: This package is no longer supported.
|
||||
|
||||
npmlog@6.0.2:
|
||||
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -32089,8 +32050,6 @@ snapshots:
|
|||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
|
||||
'@iarna/toml@2.2.5': {}
|
||||
|
||||
'@iconify-json/heroicons@1.2.3':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
|
@ -33122,21 +33081,6 @@ snapshots:
|
|||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 5.0.1
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.7.3
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)':
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
|
@ -38668,13 +38612,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
|
||||
'@sveltejs/adapter-netlify@5.2.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))':
|
||||
dependencies:
|
||||
'@iarna/toml': 2.2.5
|
||||
'@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
esbuild: 0.25.12
|
||||
set-cookie-parser: 2.7.2
|
||||
|
||||
'@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3)
|
||||
|
|
@ -38691,6 +38628,14 @@ snapshots:
|
|||
'@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
rollup: 4.53.3
|
||||
|
||||
'@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3)
|
||||
'@rollup/plugin-json': 6.1.0(rollup@4.53.3)
|
||||
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3)
|
||||
'@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
rollup: 4.53.3
|
||||
|
||||
'@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3)
|
||||
|
|
@ -39276,9 +39221,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@types/bcrypt@5.0.2':
|
||||
dependencies:
|
||||
'@types/node': 22.19.1
|
||||
'@types/bcryptjs@2.4.6': {}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
|
|
@ -41251,11 +41194,6 @@ snapshots:
|
|||
|
||||
aproba@2.1.0: {}
|
||||
|
||||
are-we-there-yet@2.0.0:
|
||||
dependencies:
|
||||
delegates: 1.0.0
|
||||
readable-stream: 3.6.2
|
||||
|
||||
are-we-there-yet@3.0.1:
|
||||
dependencies:
|
||||
delegates: 1.0.0
|
||||
|
|
@ -42042,13 +41980,7 @@ snapshots:
|
|||
dependencies:
|
||||
tweetnacl: 0.14.5
|
||||
|
||||
bcrypt@5.1.1(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
||||
node-addon-api: 5.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
bcryptjs@2.4.3: {}
|
||||
|
||||
better-auth@1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.44.0):
|
||||
dependencies:
|
||||
|
|
@ -47777,18 +47709,6 @@ snapshots:
|
|||
|
||||
fuse.js@7.1.0: {}
|
||||
|
||||
gauge@3.0.2:
|
||||
dependencies:
|
||||
aproba: 2.1.0
|
||||
color-support: 1.1.3
|
||||
console-control-strings: 1.1.0
|
||||
has-unicode: 2.0.1
|
||||
object-assign: 4.1.1
|
||||
signal-exit: 3.0.7
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wide-align: 1.1.5
|
||||
|
||||
gauge@4.0.4:
|
||||
dependencies:
|
||||
aproba: 2.1.0
|
||||
|
|
@ -50889,10 +50809,6 @@ snapshots:
|
|||
pify: 4.0.1
|
||||
semver: 5.7.2
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
|
@ -52606,8 +52522,6 @@ snapshots:
|
|||
|
||||
node-abort-controller@3.1.1: {}
|
||||
|
||||
node-addon-api@5.1.0: {}
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
node-dir@0.1.17:
|
||||
|
|
@ -52674,10 +52588,6 @@ snapshots:
|
|||
|
||||
nodemailer@7.0.12: {}
|
||||
|
||||
nopt@5.0.0:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
|
||||
nopt@6.0.0:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
|
|
@ -52709,13 +52619,6 @@ snapshots:
|
|||
dependencies:
|
||||
path-key: 4.0.0
|
||||
|
||||
npmlog@5.0.1:
|
||||
dependencies:
|
||||
are-we-there-yet: 2.0.0
|
||||
console-control-strings: 1.1.0
|
||||
gauge: 3.0.2
|
||||
set-blocking: 2.0.0
|
||||
|
||||
npmlog@6.0.2:
|
||||
dependencies:
|
||||
are-we-there-yet: 3.0.1
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
# Using node:20-slim instead of alpine for DuckDB glibc compatibility
|
||||
FROM node:20-slim AS builder
|
||||
|
||||
# Install pnpm and build tools for native modules (bcrypt)
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \
|
||||
&& apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
# Install pnpm (no build tools needed — bcryptjs is pure JS, DuckDB ships prebuilt binaries)
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"@nestjs/throttler": "^6.2.1",
|
||||
"@types/multer": "^2.0.0",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-auth": "^1.4.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^10.4.15",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
/**
|
||||
* Mock User Factory
|
||||
|
|
|
|||
|
|
@ -1500,7 +1500,7 @@ export class BetterAuthService {
|
|||
const db = getDb(this.databaseUrl);
|
||||
const { accounts } = await import('../../db/schema/auth.schema');
|
||||
const { eq, and } = await import('drizzle-orm');
|
||||
const bcrypt = await import('bcrypt');
|
||||
const bcrypt = await import('bcryptjs');
|
||||
|
||||
// Get credential account (where password is stored)
|
||||
const [account] = await db
|
||||
|
|
@ -1560,7 +1560,7 @@ export class BetterAuthService {
|
|||
const db = getDb(this.databaseUrl);
|
||||
const { accounts, users, sessions } = await import('../../db/schema/auth.schema');
|
||||
const { eq, and } = await import('drizzle-orm');
|
||||
const bcrypt = await import('bcrypt');
|
||||
const bcrypt = await import('bcryptjs');
|
||||
|
||||
// Get credential account
|
||||
const [account] = await db
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { getDb } from '../../db/connection';
|
||||
import {
|
||||
giftCodes,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue