chore: remove unused 3P dependencies (zod, gray-matter, bcryptjs) and switch to native implementations
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import matter from 'gray-matter';
|
import yaml from 'js-yaml';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { imageUrl, assetPlaceholder } from '@/lib/urls';
|
import { imageUrl, assetPlaceholder } from '@/lib/urls';
|
||||||
import { immich } from '@/lib/immich';
|
import { immich } from '@/lib/immich';
|
||||||
@@ -26,8 +26,21 @@ function getAboutContent() {
|
|||||||
return { meta: {} as AboutFrontmatter, body: '' };
|
return { meta: {} as AboutFrontmatter, body: '' };
|
||||||
}
|
}
|
||||||
const raw = readFileSync(filePath, 'utf-8');
|
const raw = readFileSync(filePath, 'utf-8');
|
||||||
const { data, content } = matter(raw);
|
|
||||||
return { meta: data as AboutFrontmatter, body: content.trim() };
|
let meta = {} as AboutFrontmatter;
|
||||||
|
let body = raw;
|
||||||
|
|
||||||
|
const match = raw.match(/^(?:---\r?\n)([\s\S]*?)(?:\r?\n---\r?\n)([\s\S]*)$/);
|
||||||
|
if (match) {
|
||||||
|
try {
|
||||||
|
meta = (yaml.load(match[1]) || {}) as AboutFrontmatter;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse about.md frontmatter', e);
|
||||||
|
}
|
||||||
|
body = match[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { meta, body: body.trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AboutPage() {
|
export default async function AboutPage() {
|
||||||
|
|||||||
49
lib/auth.ts
49
lib/auth.ts
@@ -7,18 +7,43 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { getConfig, SubpageConfig } from './config';
|
import { getConfig, SubpageConfig } from './config';
|
||||||
|
|
||||||
const TOKEN_EXPIRY_HOURS = 24;
|
const TOKEN_EXPIRY_HOURS = 24;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a string looks like a bcrypt hash.
|
* Check if a string looks like a legacy bcrypt hash.
|
||||||
*/
|
*/
|
||||||
function isHash(str: string): boolean {
|
function isBcryptHash(str: string): boolean {
|
||||||
return str.startsWith('$2a$') || str.startsWith('$2b$') || str.startsWith('$2y$');
|
return str.startsWith('$2a$') || str.startsWith('$2b$') || str.startsWith('$2y$');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native SCrypt verify (format: 'scrypt:salt:hash_hex')
|
||||||
|
*/
|
||||||
|
function verifyScrypt(password: string, stored: string): boolean {
|
||||||
|
if (!stored.startsWith('scrypt:')) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [, salt, hashHex] = stored.split(':');
|
||||||
|
const key = crypto.scryptSync(password, salt, 64);
|
||||||
|
const expectedKey = Buffer.from(hashHex, 'hex');
|
||||||
|
if (key.length !== expectedKey.length) return false;
|
||||||
|
return crypto.timingSafeEqual(key, expectedKey);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to generate an scrypt string to print in logs.
|
||||||
|
*/
|
||||||
|
function generateScryptHash(password: string): string {
|
||||||
|
const salt = crypto.randomBytes(16).toString('hex');
|
||||||
|
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
|
||||||
|
return `scrypt:${salt}:${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
function hmac(data: string): string {
|
function hmac(data: string): string {
|
||||||
return crypto.createHmac('sha256', getConfig().authSecret).update(data).digest('hex');
|
return crypto.createHmac('sha256', getConfig().authSecret).update(data).digest('hex');
|
||||||
}
|
}
|
||||||
@@ -56,15 +81,27 @@ export function authenticate(slug: string, password: string): string | null {
|
|||||||
if (!sp?.password) return null;
|
if (!sp?.password) return null;
|
||||||
|
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
if (isHash(sp.password)) {
|
|
||||||
isValid = bcrypt.compareSync(password, sp.password);
|
if (isBcryptHash(sp.password)) {
|
||||||
|
console.error(
|
||||||
|
`\n❌ SECURITY ERROR: Subpage "${slug}" is using an outdated bcrypt password hash.\n` +
|
||||||
|
` Bcrypt dependency has been removed to reduce bundle size.\n` +
|
||||||
|
` Please switch temporarily to plaintext in your gallery.yaml, log in again\n` +
|
||||||
|
` to see your new secure "scrypt:..." hash in the logs, and update your file.\n`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sp.password.startsWith('scrypt:')) {
|
||||||
|
isValid = verifyScrypt(password, sp.password);
|
||||||
} else {
|
} else {
|
||||||
// Plaintext fallback (deprecated)
|
// Plaintext fallback (deprecated)
|
||||||
isValid = password === sp.password;
|
isValid = password === sp.password;
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
|
const recommendedHash = generateScryptHash(sp.password);
|
||||||
console.warn(
|
console.warn(
|
||||||
`\n⚠️ SECURITY WARNING: Subpage "${slug}" is using a plaintext password in gallery.yaml.\n` +
|
`\n⚠️ SECURITY WARNING: Subpage "${slug}" is using a plaintext password in gallery.yaml.\n` +
|
||||||
` Please hash it for better security. You can use: npx bcryptjs ${sp.password}\n`,
|
` Please replace it with this native secure hash:\n\n ${recommendedHash}\n`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
lib/env.ts
86
lib/env.ts
@@ -1,50 +1,58 @@
|
|||||||
/**
|
/**
|
||||||
* Zod-validated environment variables.
|
* Environment variables parser.
|
||||||
* Parsed once at startup — all env access should go through this module.
|
* Parsed once at startup — all env access should go through this module.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
export interface Env {
|
||||||
|
IMMICH_API_URL: string;
|
||||||
const envSchema = z.object({
|
IMMICH_API_KEY: string;
|
||||||
/** Base URL of your Immich instance, e.g. https://immich.example.com */
|
SITE_TITLE: string;
|
||||||
IMMICH_API_URL: z
|
SITE_SUBTITLE: string;
|
||||||
.string()
|
CACHE_TTL: number;
|
||||||
.url('IMMICH_API_URL must be a valid URL')
|
RATE_LIMIT_RPM: number;
|
||||||
.transform((url) => url.replace(/\/+$/, '')),
|
AUTH_SECRET?: string;
|
||||||
|
}
|
||||||
/** Immich API key (from Admin → API Keys) */
|
|
||||||
IMMICH_API_KEY: z.string().min(1, 'IMMICH_API_KEY is required'),
|
|
||||||
|
|
||||||
/** Public site title shown in the header */
|
|
||||||
SITE_TITLE: z.string().default('Gallery'),
|
|
||||||
|
|
||||||
/** Subtitle displayed below the title */
|
|
||||||
SITE_SUBTITLE: z.string().default(''),
|
|
||||||
|
|
||||||
/** Cache TTL in seconds */
|
|
||||||
CACHE_TTL: z.coerce.number().int().min(0).default(300),
|
|
||||||
|
|
||||||
/** Rate limit: max requests per minute per IP */
|
|
||||||
RATE_LIMIT_RPM: z.coerce.number().int().min(1).default(600),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Secret used for signing auth cookies and encrypting asset tokens.
|
|
||||||
* If not provided, IMMICH_API_KEY will be used as a fallback (configured in config.ts).
|
|
||||||
*/
|
|
||||||
AUTH_SECRET: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Env = z.infer<typeof envSchema>;
|
|
||||||
|
|
||||||
function parseEnv(): Env {
|
function parseEnv(): Env {
|
||||||
const result = envSchema.safeParse(process.env);
|
const errors: string[] = [];
|
||||||
if (!result.success) {
|
|
||||||
const formatted = result.error.issues
|
const urlRaw = process.env.IMMICH_API_URL;
|
||||||
.map((issue) => ` ✗ ${issue.path.join('.')}: ${issue.message}`)
|
let apiUrl = '';
|
||||||
.join('\n');
|
if (!urlRaw) {
|
||||||
|
errors.push('IMMICH_API_URL is required');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
apiUrl = new URL(urlRaw).toString().replace(/\/+$/, '');
|
||||||
|
} catch {
|
||||||
|
errors.push('IMMICH_API_URL must be a valid URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = process.env.IMMICH_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
errors.push('IMMICH_API_KEY is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const formatted = errors.map((err) => ` ✗ ${err}`).join('\n');
|
||||||
throw new Error(`\n❌ Environment validation failed:\n${formatted}\n`);
|
throw new Error(`\n❌ Environment validation failed:\n${formatted}\n`);
|
||||||
}
|
}
|
||||||
return result.data;
|
|
||||||
|
const cacheTtlStr = process.env.CACHE_TTL;
|
||||||
|
const cacheTtl = cacheTtlStr && !isNaN(parseInt(cacheTtlStr, 10)) ? parseInt(cacheTtlStr, 10) : 300;
|
||||||
|
|
||||||
|
const rateLimitStr = process.env.RATE_LIMIT_RPM;
|
||||||
|
const rateLimit = rateLimitStr && !isNaN(parseInt(rateLimitStr, 10)) ? parseInt(rateLimitStr, 10) : 600;
|
||||||
|
|
||||||
|
return {
|
||||||
|
IMMICH_API_URL: apiUrl,
|
||||||
|
IMMICH_API_KEY: apiKey as string,
|
||||||
|
SITE_TITLE: process.env.SITE_TITLE || 'Gallery',
|
||||||
|
SITE_SUBTITLE: process.env.SITE_SUBTITLE || '',
|
||||||
|
CACHE_TTL: Math.max(0, cacheTtl),
|
||||||
|
RATE_LIMIT_RPM: Math.max(1, rateLimit),
|
||||||
|
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validated, typed environment variables. */
|
/** Validated, typed environment variables. */
|
||||||
|
|||||||
132
package-lock.json
generated
132
package-lock.json
generated
@@ -7,21 +7,18 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "immich-lightbox",
|
"name": "immich-lightbox",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"bcryptjs": "^3.0.3",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"thumbhash": "^0.1.1",
|
"thumbhash": "^0.1.1"
|
||||||
"zod": "^4.3.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -2091,13 +2088,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/bcryptjs": {
|
|
||||||
"version": "2.4.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
|
||||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -3199,15 +3189,6 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bcryptjs": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"bin": {
|
|
||||||
"bcrypt": "bin/bcrypt"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -4283,19 +4264,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esprima": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"bin": {
|
|
||||||
"esparse": "bin/esparse.js",
|
|
||||||
"esvalidate": "bin/esvalidate.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
@@ -4362,18 +4330,6 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/extend-shallow": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-extendable": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4724,43 +4680,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gray-matter": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^3.13.1",
|
|
||||||
"kind-of": "^6.0.2",
|
|
||||||
"section-matter": "^1.0.0",
|
|
||||||
"strip-bom-string": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/argparse": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sprintf-js": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/js-yaml": {
|
|
||||||
"version": "3.14.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
|
||||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^1.0.7",
|
|
||||||
"esprima": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||||
@@ -5089,15 +5008,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-extendable": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -5518,15 +5428,6 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/kind-of": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/language-subtag-registry": {
|
"node_modules/language-subtag-registry": {
|
||||||
"version": "0.3.23",
|
"version": "0.3.23",
|
||||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
|
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
|
||||||
@@ -6517,19 +6418,6 @@
|
|||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/section-matter": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"extend-shallow": "^2.0.1",
|
|
||||||
"kind-of": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -6762,12 +6650,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sprintf-js": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@@ -6926,15 +6808,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-bom-string": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -7765,6 +7638,7 @@
|
|||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -16,19 +16,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"bcryptjs": "^3.0.3",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"thumbhash": "^0.1.1",
|
"thumbhash": "^0.1.1"
|
||||||
"zod": "^4.3.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
Reference in New Issue
Block a user