feat: implement structured logging for server and client
Server: - Add pino and pino-http for structured logging - Create @Logged class decorator using Proxy pattern for automatic method logging - Add pino redact config for sensitive data (password, token, etc.) - Move AuthMiddleware to controllers folder (per architecture diagram) - Add LoggingMiddleware for HTTP request logging - Replace console.log/error with structured logger in controllers and app.ts - Decorate all repositories and GPTAdapter with @Logged Client: - Add react-native-logs with namespaced loggers (apiLogger, storeLogger) - Add request/response logging to ApiClient with duration tracking
This commit is contained in:
51
CLAUDE.md
51
CLAUDE.md
@@ -47,6 +47,8 @@ npm run start -w @caldav/server # Run compiled server (port 3000)
|
|||||||
| | Mongoose | ODM |
|
| | Mongoose | ODM |
|
||||||
| | GPT (OpenAI) | AI/LLM for chat |
|
| | GPT (OpenAI) | AI/LLM for chat |
|
||||||
| | JWT | Authentication |
|
| | JWT | Authentication |
|
||||||
|
| | pino / pino-http | Structured logging |
|
||||||
|
| | react-native-logs | Client-side logging |
|
||||||
| Planned | iCalendar | Event export/import |
|
| Planned | iCalendar | Event export/import |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -83,9 +85,12 @@ src/
|
|||||||
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal
|
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal
|
||||||
│ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons)
|
│ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons)
|
||||||
├── Themes.tsx # Centralized color/theme definitions
|
├── Themes.tsx # Centralized color/theme definitions
|
||||||
|
├── logging/
|
||||||
|
│ ├── index.ts # Re-exports
|
||||||
|
│ └── logger.ts # react-native-logs config (apiLogger, storeLogger)
|
||||||
├── services/
|
├── services/
|
||||||
│ ├── index.ts # Re-exports all services
|
│ ├── index.ts # Re-exports all services
|
||||||
│ ├── ApiClient.ts # HTTP client (get, post, put, delete)
|
│ ├── ApiClient.ts # HTTP client with request logging (get, post, put, delete)
|
||||||
│ ├── AuthService.ts # login(), register(), logout(), refresh()
|
│ ├── AuthService.ts # login(), register(), logout(), refresh()
|
||||||
│ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete()
|
│ ├── EventService.ts # getAll(), getById(), getByDateRange(), create(), update(), delete()
|
||||||
│ └── ChatService.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation()
|
│ └── ChatService.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation()
|
||||||
@@ -103,12 +108,16 @@ src/
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── app.ts # Entry point, DI setup, Express config
|
├── app.ts # Entry point, DI setup, Express config
|
||||||
├── controllers/ # Request handlers
|
├── controllers/ # Request handlers + middleware (per architecture diagram)
|
||||||
│ ├── AuthController.ts # login(), register(), refresh(), logout()
|
│ ├── AuthController.ts # login(), register(), refresh(), logout()
|
||||||
│ ├── ChatController.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation()
|
│ ├── ChatController.ts # sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation()
|
||||||
│ └── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete()
|
│ ├── EventController.ts # create(), getById(), getAll(), getByDateRange(), update(), delete()
|
||||||
├── middleware/
|
│ ├── AuthMiddleware.ts # authenticate() - JWT validation
|
||||||
│ └── AuthMiddleware.ts # authenticate() - JWT validation
|
│ └── LoggingMiddleware.ts # httpLogger - pino-http request logging
|
||||||
|
├── logging/
|
||||||
|
│ ├── index.ts # Re-exports
|
||||||
|
│ ├── logger.ts # pino config with redact for sensitive data
|
||||||
|
│ └── Logged.ts # @Logged() class decorator for automatic method logging
|
||||||
├── routes/ # API endpoint definitions
|
├── routes/ # API endpoint definitions
|
||||||
│ ├── index.ts # Combines all routes under /api
|
│ ├── index.ts # Combines all routes under /api
|
||||||
│ ├── auth.routes.ts # /api/auth/*
|
│ ├── auth.routes.ts # /api/auth/*
|
||||||
@@ -239,6 +248,32 @@ const Schema = new Schema<Doc, Model<Doc, {}, {}, IdVirtual>, {}, {}, IdVirtual>
|
|||||||
|
|
||||||
Repositories use `doc.toJSON() as unknown as Type` casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output).
|
Repositories use `doc.toJSON() as unknown as Type` casting (required because Mongoose's TypeScript types don't reflect virtual fields in toJSON output).
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Structured logging with pino (server) and react-native-logs (client).
|
||||||
|
|
||||||
|
**Server Logging:**
|
||||||
|
- `pino` with `pino-pretty` for development, JSON in production
|
||||||
|
- `pino-http` middleware logs all HTTP requests (method, path, status, duration)
|
||||||
|
- `@Logged()` class decorator for automatic method logging on repositories and services
|
||||||
|
- Sensitive data (password, token, etc.) automatically redacted via pino's `redact` config
|
||||||
|
|
||||||
|
**@Logged Decorator Pattern:**
|
||||||
|
```typescript
|
||||||
|
@Logged("MongoEventRepository")
|
||||||
|
export class MongoEventRepository implements EventRepository { ... }
|
||||||
|
|
||||||
|
@Logged("GPTAdapter")
|
||||||
|
export class GPTAdapter implements AIProvider { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
The decorator uses a Proxy to intercept method calls lazily, preserves sync/async nature, and logs start/completion/failure with duration.
|
||||||
|
|
||||||
|
**Client Logging:**
|
||||||
|
- `react-native-logs` with namespaced loggers (apiLogger, storeLogger)
|
||||||
|
- ApiClient logs all requests with method, endpoint, status, duration
|
||||||
|
- Log level: debug in __DEV__, warn in production
|
||||||
|
|
||||||
## MVP Feature Scope
|
## MVP Feature Scope
|
||||||
|
|
||||||
### Must-Have
|
### Must-Have
|
||||||
@@ -274,6 +309,8 @@ JWT_EXPIRES_IN=1h
|
|||||||
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||||
OPENAI_API_KEY=sk-proj-...
|
OPENAI_API_KEY=sk-proj-...
|
||||||
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
||||||
|
LOG_LEVEL=debug # debug | info | warn | error | fatal
|
||||||
|
NODE_ENV=development # development = pretty logs, production = JSON
|
||||||
```
|
```
|
||||||
|
|
||||||
## Current Implementation Status
|
## Current Implementation Status
|
||||||
@@ -299,6 +336,8 @@ USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
|||||||
- `ChatRepository` interface: updateMessage() added for respondedAction tracking
|
- `ChatRepository` interface: updateMessage() added for respondedAction tracking
|
||||||
- `GPTAdapter`: Full implementation with OpenAI GPT (gpt-5-mini model), function calling for calendar operations
|
- `GPTAdapter`: Full implementation with OpenAI GPT (gpt-5-mini model), function calling for calendar operations
|
||||||
- `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter)
|
- `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter)
|
||||||
|
- `logging/`: Structured logging with pino, pino-http middleware, @Logged decorator
|
||||||
|
- All repositories and GPTAdapter decorated with @Logged for automatic method logging
|
||||||
- **Stubbed (TODO):**
|
- **Stubbed (TODO):**
|
||||||
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
||||||
- `AuthController`: refresh(), logout()
|
- `AuthController`: refresh(), logout()
|
||||||
@@ -323,7 +362,7 @@ USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
|||||||
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
||||||
- Auto-scroll to end on new messages and keyboard show
|
- Auto-scroll to end on new messages and keyboard show
|
||||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||||
- `ApiClient`: get(), post(), put(), delete() implemented
|
- `ApiClient`: get(), post(), put(), delete() implemented with request/response logging
|
||||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented
|
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented
|
||||||
- `ChatService`: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination
|
- `ChatService`: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination
|
||||||
- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
|
- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-logs": "^5.5.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "5.6.0",
|
"react-native-safe-area-context": "5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
|
|||||||
1
apps/client/src/logging/index.ts
Normal file
1
apps/client/src/logging/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { log, apiLogger, storeLogger } from "./logger";
|
||||||
30
apps/client/src/logging/logger.ts
Normal file
30
apps/client/src/logging/logger.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { logger, consoleTransport } from "react-native-logs";
|
||||||
|
|
||||||
|
const log = logger.createLogger({
|
||||||
|
levels: {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
},
|
||||||
|
severity: __DEV__ ? "debug" : "warn",
|
||||||
|
transport: consoleTransport,
|
||||||
|
transportOptions: {
|
||||||
|
colors: {
|
||||||
|
debug: "white",
|
||||||
|
info: "blueBright",
|
||||||
|
warn: "yellowBright",
|
||||||
|
error: "redBright",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async: true,
|
||||||
|
dateFormat: "time",
|
||||||
|
printLevel: true,
|
||||||
|
printDate: true,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiLogger = log.extend("API");
|
||||||
|
export const storeLogger = log.extend("Store");
|
||||||
|
|
||||||
|
export { log };
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import { apiLogger } from "../logging";
|
||||||
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
process.env.EXPO_PUBLIC_API_URL ||
|
process.env.EXPO_PUBLIC_API_URL ||
|
||||||
@@ -19,20 +20,33 @@ async function request<T>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
options?: RequestOptions,
|
options?: RequestOptions,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
const start = performance.now();
|
||||||
method,
|
apiLogger.debug(`${method} ${endpoint}`);
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options?.headers,
|
|
||||||
},
|
|
||||||
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
try {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = Math.round(performance.now() - start);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
apiLogger.error(`${method} ${endpoint} - ${response.status} (${duration}ms)`);
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
apiLogger.debug(`${method} ${endpoint} - ${response.status} (${duration}ms)`);
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Math.round(performance.now() - start);
|
||||||
|
apiLogger.error(`${method} ${endpoint} failed (${duration}ms): ${error}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ApiClient = {
|
export const ApiClient = {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1",
|
"mongoose": "^9.1.1",
|
||||||
"openai": "^6.15.0",
|
"openai": "^6.15.0",
|
||||||
|
"pino": "^10.1.1",
|
||||||
|
"pino-http": "^11.0.0",
|
||||||
"rrule": "^2.8.1"
|
"rrule": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
executeToolCall,
|
executeToolCall,
|
||||||
ToolDefinition,
|
ToolDefinition,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { Logged } from "../logging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert tool definitions to OpenAI format.
|
* Convert tool definitions to OpenAI format.
|
||||||
@@ -24,6 +25,7 @@ function toOpenAITools(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Logged("GPTAdapter")
|
||||||
export class GPTAdapter implements AIProvider {
|
export class GPTAdapter implements AIProvider {
|
||||||
private client: OpenAI;
|
private client: OpenAI;
|
||||||
private model: string;
|
private model: string;
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import mongoose from "mongoose";
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
|
||||||
import { createRoutes } from "./routes";
|
import { createRoutes } from "./routes";
|
||||||
import { AuthController, ChatController, EventController } from "./controllers";
|
import {
|
||||||
|
AuthController,
|
||||||
|
ChatController,
|
||||||
|
EventController,
|
||||||
|
httpLogger,
|
||||||
|
} from "./controllers";
|
||||||
import { AuthService, ChatService, EventService } from "./services";
|
import { AuthService, ChatService, EventService } from "./services";
|
||||||
import {
|
import {
|
||||||
MongoUserRepository,
|
MongoUserRepository,
|
||||||
@@ -11,6 +16,7 @@ import {
|
|||||||
MongoChatRepository,
|
MongoChatRepository,
|
||||||
} from "./repositories";
|
} from "./repositories";
|
||||||
import { GPTAdapter } from "./ai";
|
import { GPTAdapter } from "./ai";
|
||||||
|
import { logger } from "./logging";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
@@ -18,6 +24,7 @@ const mongoUri = process.env.MONGODB_URI || "mongodb://localhost:27017/caldav";
|
|||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(httpLogger);
|
||||||
|
|
||||||
// CORS - only needed for web browser development
|
// CORS - only needed for web browser development
|
||||||
// Native mobile apps don't send Origin headers and aren't affected by CORS
|
// Native mobile apps don't send Origin headers and aren't affected by CORS
|
||||||
@@ -86,7 +93,7 @@ app.post("/api/ai/test", async (req, res) => {
|
|||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("AI test error:", error);
|
logger.error({ error }, "AI test error");
|
||||||
res.status(500).json({ error: String(error) });
|
res.status(500).json({ error: String(error) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -95,13 +102,13 @@ app.post("/api/ai/test", async (req, res) => {
|
|||||||
async function start() {
|
async function start() {
|
||||||
try {
|
try {
|
||||||
await mongoose.connect(mongoUri);
|
await mongoose.connect(mongoUri);
|
||||||
console.log("Connected to MongoDB");
|
logger.info("Connected to MongoDB");
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running on port ${port}`);
|
logger.info({ port }, "Server started");
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start server:", error);
|
logger.fatal({ error }, "Failed to start server");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
GetMessagesOptions,
|
GetMessagesOptions,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ChatService } from "../services";
|
import { ChatService } from "../services";
|
||||||
import { AuthenticatedRequest } from "../middleware";
|
import { createLogger } from "../logging";
|
||||||
|
import { AuthenticatedRequest } from "./AuthMiddleware";
|
||||||
|
|
||||||
|
const log = createLogger("ChatController");
|
||||||
|
|
||||||
export class ChatController {
|
export class ChatController {
|
||||||
constructor(private chatService: ChatService) {}
|
constructor(private chatService: ChatService) {}
|
||||||
@@ -19,6 +22,7 @@ export class ChatController {
|
|||||||
const response = await this.chatService.processMessage(userId, data);
|
const response = await this.chatService.processMessage(userId, data);
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error({ error, userId: req.user?.userId }, "Error processing message");
|
||||||
res.status(500).json({ error: "Failed to process message" });
|
res.status(500).json({ error: "Failed to process message" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +48,7 @@ export class ChatController {
|
|||||||
);
|
);
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error({ error, conversationId: req.params.conversationId }, "Error confirming event");
|
||||||
res.status(500).json({ error: "Failed to confirm event" });
|
res.status(500).json({ error: "Failed to confirm event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +64,7 @@ export class ChatController {
|
|||||||
);
|
);
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error({ error, conversationId: req.params.conversationId }, "Error rejecting event");
|
||||||
res.status(500).json({ error: "Failed to reject event" });
|
res.status(500).json({ error: "Failed to reject event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +78,7 @@ export class ChatController {
|
|||||||
const conversations = await this.chatService.getConversations(userId);
|
const conversations = await this.chatService.getConversations(userId);
|
||||||
res.json(conversations);
|
res.json(conversations);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error({ error, userId: req.user?.userId }, "Error getting conversations");
|
||||||
res.status(500).json({ error: "Failed to get conversations" });
|
res.status(500).json({ error: "Failed to get conversations" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +109,7 @@ export class ChatController {
|
|||||||
if ((error as Error).message === "Conversation not found") {
|
if ((error as Error).message === "Conversation not found") {
|
||||||
res.status(404).json({ error: "Conversation not found" });
|
res.status(404).json({ error: "Conversation not found" });
|
||||||
} else {
|
} else {
|
||||||
|
log.error({ error, conversationId: req.params.id }, "Error getting conversation");
|
||||||
res.status(500).json({ error: "Failed to get conversation" });
|
res.status(500).json({ error: "Failed to get conversation" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { EventService } from "../services";
|
import { EventService } from "../services";
|
||||||
import { AuthenticatedRequest } from "../middleware";
|
import { createLogger } from "../logging";
|
||||||
|
import { AuthenticatedRequest } from "./AuthMiddleware";
|
||||||
|
|
||||||
|
const log = createLogger("EventController");
|
||||||
|
|
||||||
export class EventController {
|
export class EventController {
|
||||||
constructor(private eventService: EventService) {}
|
constructor(private eventService: EventService) {}
|
||||||
@@ -10,7 +13,7 @@ export class EventController {
|
|||||||
const event = await this.eventService.create(req.user!.userId, req.body);
|
const event = await this.eventService.create(req.user!.userId, req.body);
|
||||||
res.status(201).json(event);
|
res.status(201).json(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating event:", error);
|
log.error({ error, userId: req.user?.userId }, "Error creating event");
|
||||||
res.status(500).json({ error: "Failed to create event" });
|
res.status(500).json({ error: "Failed to create event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +30,7 @@ export class EventController {
|
|||||||
}
|
}
|
||||||
res.json(event);
|
res.json(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting event:", error);
|
log.error({ error, eventId: req.params.id }, "Error getting event");
|
||||||
res.status(500).json({ error: "Failed to get event" });
|
res.status(500).json({ error: "Failed to get event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +40,7 @@ export class EventController {
|
|||||||
const events = await this.eventService.getAll(req.user!.userId);
|
const events = await this.eventService.getAll(req.user!.userId);
|
||||||
res.json(events);
|
res.json(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting events:", error);
|
log.error({ error, userId: req.user?.userId }, "Error getting events");
|
||||||
res.status(500).json({ error: "Failed to get events" });
|
res.status(500).json({ error: "Failed to get events" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,7 @@ export class EventController {
|
|||||||
);
|
);
|
||||||
res.json(events);
|
res.json(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting events by range:", error);
|
log.error({ error, start: req.query.start, end: req.query.end }, "Error getting events by range");
|
||||||
res.status(500).json({ error: "Failed to get events" });
|
res.status(500).json({ error: "Failed to get events" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +90,7 @@ export class EventController {
|
|||||||
}
|
}
|
||||||
res.json(event);
|
res.json(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating event:", error);
|
log.error({ error, eventId: req.params.id }, "Error updating event");
|
||||||
res.status(500).json({ error: "Failed to update event" });
|
res.status(500).json({ error: "Failed to update event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +107,7 @@ export class EventController {
|
|||||||
}
|
}
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting event:", error);
|
log.error({ error, eventId: req.params.id }, "Error deleting event");
|
||||||
res.status(500).json({ error: "Failed to delete event" });
|
res.status(500).json({ error: "Failed to delete event" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
apps/server/src/controllers/LoggingMiddleware.ts
Normal file
27
apps/server/src/controllers/LoggingMiddleware.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import pinoHttp from "pino-http";
|
||||||
|
import { logger } from "../logging";
|
||||||
|
|
||||||
|
export const httpLogger = pinoHttp({
|
||||||
|
logger,
|
||||||
|
customLogLevel: (_req, res, err) => {
|
||||||
|
if (res.statusCode >= 500 || err) return "error";
|
||||||
|
if (res.statusCode >= 400) return "warn";
|
||||||
|
return "info";
|
||||||
|
},
|
||||||
|
customSuccessMessage: (req, res) => {
|
||||||
|
return `${req.method} ${req.url} ${res.statusCode}`;
|
||||||
|
},
|
||||||
|
customErrorMessage: (req, _res, err) => {
|
||||||
|
return `${req.method} ${req.url} failed: ${err.message}`;
|
||||||
|
},
|
||||||
|
redact: ["req.headers.authorization"],
|
||||||
|
serializers: {
|
||||||
|
req: (req) => ({
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
}),
|
||||||
|
res: (res) => ({
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from "./AuthController";
|
export * from "./AuthController";
|
||||||
export * from "./ChatController";
|
export * from "./ChatController";
|
||||||
export * from "./EventController";
|
export * from "./EventController";
|
||||||
|
export * from "./AuthMiddleware";
|
||||||
|
export * from "./LoggingMiddleware";
|
||||||
|
|||||||
76
apps/server/src/logging/Logged.ts
Normal file
76
apps/server/src/logging/Logged.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { createLogger } from "./logger";
|
||||||
|
|
||||||
|
export function Logged(name: string) {
|
||||||
|
const log = createLogger(name);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return function <T extends { new (...args: any[]): any }>(Constructor: T) {
|
||||||
|
return class extends Constructor {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
// Return a Proxy that intercepts method calls lazily
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, propKey, receiver) {
|
||||||
|
const original = Reflect.get(target, propKey, receiver);
|
||||||
|
|
||||||
|
if (typeof original !== "function" || propKey === "constructor") {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const originalFn = original as (...args: any[]) => any;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return function (this: any, ...methodArgs: any[]) {
|
||||||
|
const start = performance.now();
|
||||||
|
const method = String(propKey);
|
||||||
|
|
||||||
|
// Pino's redact handles sanitization - just pass args directly
|
||||||
|
log.debug({ method, args: methodArgs }, `${method} started`);
|
||||||
|
|
||||||
|
const logCompletion = (err?: unknown) => {
|
||||||
|
const duration = Math.round(performance.now() - start);
|
||||||
|
if (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : String(err);
|
||||||
|
log.error(
|
||||||
|
{ method, duration, error: message },
|
||||||
|
`${method} failed`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info({ method, duration }, `${method} completed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = originalFn.apply(this, methodArgs);
|
||||||
|
|
||||||
|
// Check if async - preserves sync/async nature of method
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return result
|
||||||
|
.then((val) => {
|
||||||
|
logCompletion();
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logCompletion(err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronous completion
|
||||||
|
logCompletion();
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
logCompletion(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
2
apps/server/src/logging/index.ts
Normal file
2
apps/server/src/logging/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { logger, createLogger, type Logger } from "./logger";
|
||||||
|
export { Logged } from "./Logged";
|
||||||
43
apps/server/src/logging/logger.ts
Normal file
43
apps/server/src/logging/logger.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
|
export const logger = pino({
|
||||||
|
level: process.env.LOG_LEVEL || (isDevelopment ? "debug" : "info"),
|
||||||
|
redact: {
|
||||||
|
paths: [
|
||||||
|
// Root level
|
||||||
|
"password",
|
||||||
|
"passwordHash",
|
||||||
|
"token",
|
||||||
|
// One level deep (e.g. user.password)
|
||||||
|
"*.password",
|
||||||
|
"*.passwordHash",
|
||||||
|
"*.token",
|
||||||
|
// In arrays (for 'args' in decorator)
|
||||||
|
"args[*].password",
|
||||||
|
"args[*].passwordHash",
|
||||||
|
"args[*].token",
|
||||||
|
],
|
||||||
|
censor: "[REDACTED]",
|
||||||
|
},
|
||||||
|
transport: isDevelopment
|
||||||
|
? {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
translateTime: "SYS:HH:MM:ss",
|
||||||
|
ignore: "pid,hostname",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
base: {
|
||||||
|
service: "caldav-server",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createLogger(module: string) {
|
||||||
|
return logger.child({ module });
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Logger = pino.Logger;
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./AuthMiddleware";
|
|
||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
UpdateMessageDTO,
|
UpdateMessageDTO,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ChatRepository } from "../../services/interfaces";
|
import { ChatRepository } from "../../services/interfaces";
|
||||||
|
import { Logged } from "../../logging";
|
||||||
import { ChatMessageModel, ConversationModel } from "./models";
|
import { ChatMessageModel, ConversationModel } from "./models";
|
||||||
|
|
||||||
|
@Logged("MongoChatRepository")
|
||||||
export class MongoChatRepository implements ChatRepository {
|
export class MongoChatRepository implements ChatRepository {
|
||||||
// Conversations
|
// Conversations
|
||||||
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
import { CalendarEvent, CreateEventDTO, UpdateEventDTO } from "@caldav/shared";
|
||||||
import { EventRepository } from "../../services/interfaces";
|
import { EventRepository } from "../../services/interfaces";
|
||||||
|
import { Logged } from "../../logging";
|
||||||
import { EventModel } from "./models";
|
import { EventModel } from "./models";
|
||||||
|
|
||||||
|
@Logged("MongoEventRepository")
|
||||||
export class MongoEventRepository implements EventRepository {
|
export class MongoEventRepository implements EventRepository {
|
||||||
async findById(id: string): Promise<CalendarEvent | null> {
|
async findById(id: string): Promise<CalendarEvent | null> {
|
||||||
const event = await EventModel.findById(id);
|
const event = await EventModel.findById(id);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { User } from "@caldav/shared";
|
import { User } from "@caldav/shared";
|
||||||
import { UserRepository, CreateUserData } from "../../services/interfaces";
|
import { UserRepository, CreateUserData } from "../../services/interfaces";
|
||||||
|
import { Logged } from "../../logging";
|
||||||
import { UserModel } from "./models";
|
import { UserModel } from "./models";
|
||||||
|
|
||||||
|
@Logged("MongoUserRepository")
|
||||||
export class MongoUserRepository implements UserRepository {
|
export class MongoUserRepository implements UserRepository {
|
||||||
async findById(id: string): Promise<User | null> {
|
async findById(id: string): Promise<User | null> {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ChatController } from "../controllers";
|
import { ChatController, authenticate } from "../controllers";
|
||||||
import { authenticate } from "../middleware";
|
|
||||||
|
|
||||||
export function createChatRoutes(chatController: ChatController): Router {
|
export function createChatRoutes(chatController: ChatController): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { EventController } from "../controllers";
|
import { EventController, authenticate } from "../controllers";
|
||||||
import { authenticate } from "../middleware";
|
|
||||||
|
|
||||||
export function createEventRoutes(eventController: EventController): Router {
|
export function createEventRoutes(eventController: EventController): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
277
package-lock.json
generated
277
package-lock.json
generated
@@ -44,6 +44,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-logs": "^5.5.0",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "5.6.0",
|
"react-native-safe-area-context": "5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
@@ -69,6 +70,8 @@
|
|||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1",
|
"mongoose": "^9.1.1",
|
||||||
"openai": "^6.15.0",
|
"openai": "^6.15.0",
|
||||||
|
"pino": "^10.1.1",
|
||||||
|
"pino-http": "^11.0.0",
|
||||||
"rrule": "^2.8.1"
|
"rrule": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
@@ -3317,6 +3321,12 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pinojs/redact": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
@@ -5136,6 +5146,15 @@
|
|||||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/atomic-sleep": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -5957,6 +5976,13 @@
|
|||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "2.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||||
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
@@ -6249,6 +6275,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dateformat": {
|
||||||
|
"version": "4.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||||
|
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -6493,6 +6529,16 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/env-editor": {
|
"node_modules/env-editor": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||||
@@ -7896,6 +7942,13 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-copy": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -7943,6 +7996,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-safe-stringify": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
@@ -8552,6 +8612,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hermes-estree": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.29.1",
|
"version": "0.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz",
|
||||||
@@ -9507,6 +9574,16 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/joycon": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -11174,6 +11251,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-exit-leak-free": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@@ -11545,6 +11631,93 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pino": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-3qqVfpJtRQUCAOs4rTOEwLH6mwJJ/CSAlbis8fKOiMzTtXh0HN/VLsn3UWVTJ7U8DsWmxeNon2IpGb+wORXH4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pinojs/redact": "^0.4.0",
|
||||||
|
"atomic-sleep": "^1.0.0",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^3.0.0",
|
||||||
|
"pino-std-serializers": "^7.0.0",
|
||||||
|
"process-warning": "^5.0.0",
|
||||||
|
"quick-format-unescaped": "^4.0.3",
|
||||||
|
"real-require": "^0.2.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"sonic-boom": "^4.0.1",
|
||||||
|
"thread-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-abstract-transport": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-http": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"pino": "^10.0.0",
|
||||||
|
"pino-std-serializers": "^7.0.0",
|
||||||
|
"process-warning": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-pretty": {
|
||||||
|
"version": "13.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
|
||||||
|
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"colorette": "^2.0.7",
|
||||||
|
"dateformat": "^4.6.3",
|
||||||
|
"fast-copy": "^4.0.0",
|
||||||
|
"fast-safe-stringify": "^2.1.1",
|
||||||
|
"help-me": "^5.0.0",
|
||||||
|
"joycon": "^3.1.1",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^3.0.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"secure-json-parse": "^4.0.0",
|
||||||
|
"sonic-boom": "^4.0.1",
|
||||||
|
"strip-json-comments": "^5.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino-pretty": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-pretty/node_modules/strip-json-comments": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-std-serializers": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
@@ -11897,6 +12070,22 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-warning": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
@@ -11960,6 +12149,17 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -12039,6 +12239,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quick-format-unescaped": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -12557,6 +12763,12 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-logs": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-logs/-/react-native-logs-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-H3Jc1pNTzNhYb9yHuk1drHdyGHwRvt4IERSz3EUul8vVTey6999fzGRFLK6ugrxYnmw7P+5fo/mRzDXeByhA8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-native-reanimated": {
|
"node_modules/react-native-reanimated": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
||||||
@@ -12840,6 +13052,15 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/real-require": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -13233,6 +13454,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -13251,6 +13481,23 @@
|
|||||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/secure-json-parse": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -13686,6 +13933,15 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonic-boom": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
@@ -13741,6 +13997,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@@ -14249,6 +14514,18 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thread-stream": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/throat": {
|
"node_modules/throat": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user