From 0cfed9530f6e49a5ab1b332e1cb475f2cf9dc0ea Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 22 Jul 2025 08:15:33 -0700 Subject: [PATCH] feat(pockethost): webhooks --- packages/dashboard/package.json | 1 + .../instances/[instanceId]/+layout.svelte | 3 + .../[instanceId]/webhooks/+page.svelte | 20 ++ .../[instanceId]/webhooks/Form.svelte | 278 ++++++++++++++++++ .../[instanceId]/webhooks/Inner.svelte | 53 ++++ .../[instanceId]/webhooks/List.svelte | 123 ++++++++ .../instances/[instanceId]/webhooks/stores.ts | 63 ++++ .../src/routes/(static)/docs/+layout.svelte | 1 + .../routes/(static)/docs/webhooks/+page.md | 262 +++++++++++++++++ .../pockethost/src/common/schema/Instance.ts | 7 + .../src/mothership-app/pb_hooks/mothership.js | 5 +- .../1753050291_updated_instances.js | 29 ++ .../instance/api/HandleInstanceUpdate.ts | 7 +- .../src/mothership-app/tsconfig.json | 1 + .../src/services/CronService/index.ts | 118 ++++++++ pnpm-lock.yaml | 189 +++--------- 16 files changed, 1003 insertions(+), 157 deletions(-) create mode 100644 packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/+page.svelte create mode 100644 packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Form.svelte create mode 100644 packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Inner.svelte create mode 100644 packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/List.svelte create mode 100644 packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/stores.ts create mode 100644 packages/dashboard/src/routes/(static)/docs/webhooks/+page.md create mode 100644 packages/pockethost/src/mothership-app/pb_migrations/1753050291_updated_instances.js create mode 100644 packages/pockethost/src/services/CronService/index.ts diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 208bea38..5b092395 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -31,6 +31,7 @@ "@types/d3-scale-chromatic": "^3.1.0", "@types/js-cookie": "^3.0.6", "autoprefixer": "^10.4.21", + "cron-parser": "^5.3.0", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "daisyui": "^4.12.24", diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/+layout.svelte b/packages/dashboard/src/routes/(app)/instances/[instanceId]/+layout.svelte index 1e2a5007..3884c438 100644 --- a/packages/dashboard/src/routes/(app)/instances/[instanceId]/+layout.svelte +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/+layout.svelte @@ -88,6 +88,9 @@
  • Secrets
  • +
  • + Webhooks +
  • Logs
  • diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/+page.svelte b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/+page.svelte new file mode 100644 index 00000000..4f1615fb --- /dev/null +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/+page.svelte @@ -0,0 +1,20 @@ + + + + {subdomain} webhooks - PocketHost + + +
    + Webhooks + +
    diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Form.svelte b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Form.svelte new file mode 100644 index 00000000..a99070a4 --- /dev/null +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Form.svelte @@ -0,0 +1,278 @@ + + +

    Add New Webhook

    + +
    + {#if successfulSave} + + {/if} + + + +
    +
    + +
    + 0 ? 'input-error text-error' : ''}`} + /> + {#if !isCronValueValid && cronValue.length > 0} +
    + + Please enter a valid cron expression (e.g., 0 9 * * 1-5 for weekdays at 9 AM UTC, or + @daily for daily at midnight UTC). + +
    + {/if} +
    + +
    + +
    +
    + +
    +

    + The schedule is a cron expression that defines when the webhook will be called in UTC time. For + example, + 0 0 * * * means every day at midnight UTC. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldAllowed ValuesSpecial Characters
    Minute0-59, - * /
    Hour0-23, - * /
    Day of Month1-31, - * / ? L + W
    Month1-12 or JAN-DEC, - * /
    Day of Week0-6 or SUN-SAT, - * / ? L + #
    +
    + Common Macros: +
      +
    • @yearly: 0 0 1 1 * (once a year at midnight, Jan 1)
    • +
    • @annually: 0 0 1 1 * (same as @yearly)
    • +
    • @monthly: 0 0 1 * * (once a month at midnight, first day)
    • +
    • @weekly: 0 0 * * 0 (once a week at midnight, Sunday)
    • +
    • @daily: 0 0 * * * (once a day at midnight)
    • +
    • @midnight: 0 0 * * * (same as @daily)
    • +
    • @hourly: 0 * * * * (once an hour at minute 0)
    • +
    +
    + +
    + Practical Examples: +
      +
    • 0 9 * * 1-5 - Every weekday at 9:00 AM
    • +
    • 0 12 * * 1 - Every Monday at noon
    • +
    • 0 0 1 * * - First day of every month at midnight
    • +
    • 0 18 * * 5 - Every Friday at 6:00 PM
    • +
    • 30 2 * * * - Every day at 2:30 AM
    • +
    • 0 */6 * * * - Every 6 hours (00:00, 06:00, 12:00, 18:00)
    • +
    • 0 0 * * 0 - Every Sunday at midnight
    • +
    • 0 8 15 * * - 15th of every month at 8:00 AM
    • +
    • 0 0 1 1 * - New Year's Day at midnight
    • +
    • 0 12 * * 0,6 - Weekends at noon
    • +
    +
    +
    +
    +
    + + diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Inner.svelte b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Inner.svelte new file mode 100644 index 00000000..9b90ef1d --- /dev/null +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/Inner.svelte @@ -0,0 +1,53 @@ + + +
    + Webhooks let you call API endpoints on your instance at scheduled times, replacing PocketBase's standard cron + scheduler. +
    + + +{#if $items.length > 0} +
    + +
    +{/if} + +{#if $items.length === 0} +
    + + No webhooks yet. Create your first webhook to get started. +
    +{:else} + +{/if} +
    diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/List.svelte b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/List.svelte new file mode 100644 index 00000000..4cb1ce26 --- /dev/null +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/List.svelte @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + {#each $items as item} + + + + + + + + {#if item.lastFired && expandedWebhooks.has(item.endpoint)} + + + + {/if} + {/each} + +
    API EndpointScheduleStatusActions
    {item.endpoint}{item.value} + {#if item.lastFired} + + {:else} + No runs yet + {/if} + + +
    +
    +
    +

    Last Execution Details

    + {formatTimestamp(item.lastFired.timestamp)} +
    + +
    +
    + +
    + + {item.lastFired.response.status} + +
    +
    + +
    + +
    +
    {item.lastFired.response
    +                        .body || '(empty)'}
    +
    +
    +
    +
    +
    diff --git a/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/stores.ts b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/stores.ts new file mode 100644 index 00000000..39702967 --- /dev/null +++ b/packages/dashboard/src/routes/(app)/instances/[instanceId]/webhooks/stores.ts @@ -0,0 +1,63 @@ +import { scaleOrdinal } from 'd3-scale' +import { schemeTableau10 } from 'd3-scale-chromatic' +import { type InstanceWebhookCollection, type InstanceWebhookItem } from 'pockethost/common' +import { writable } from 'svelte/store' + +// color scale used in both visualizations +const colorScale = scaleOrdinal(schemeTableau10) + +// Use the proper types from the schema +export type CronItem = InstanceWebhookItem +export type CronArray = InstanceWebhookCollection + +// function to sort the input array and add a color according to the sorted values +function formatInput(input: CronArray): CronArray { + return input + .sort((a, b) => (a.endpoint < b.endpoint ? -1 : 1)) + .map(({ endpoint, value, lastFired }) => ({ + endpoint, + value, + lastFired, + })) +} + +const sanitize = (item: CronItem) => { + return { + endpoint: item.endpoint.trim(), + value: item.value.trim(), + lastFired: item.lastFired, + } +} + +// create a custom store fulfilling the CRUD operations +function createItems(initialItems: CronArray) { + const { subscribe, set, update } = writable(initialItems) + + const api = { + subscribe, + clear: () => { + set([]) + }, + // create: add an object for the item at the end of the store's array + upsert: (item: CronItem) => { + const { endpoint, value, lastFired } = sanitize(item) + + return update((n) => { + return formatInput([...n.filter((i) => i.endpoint !== endpoint), { endpoint, value, lastFired }]) + }) + }, + + // delete: remove the item from the array + delete: (name: string) => { + return update((n) => { + const index = n.findIndex((item) => item.endpoint === name) + n = [...n.slice(0, index), ...n.slice(index + 1)] + return formatInput(n) + }) + }, + } + + return api +} + +export const items = createItems(formatInput([])) diff --git a/packages/dashboard/src/routes/(static)/docs/+layout.svelte b/packages/dashboard/src/routes/(static)/docs/+layout.svelte index c1f8dc9d..0f1dfb47 100644 --- a/packages/dashboard/src/routes/(static)/docs/+layout.svelte +++ b/packages/dashboard/src/routes/(static)/docs/+layout.svelte @@ -32,6 +32,7 @@ + diff --git a/packages/dashboard/src/routes/(static)/docs/webhooks/+page.md b/packages/dashboard/src/routes/(static)/docs/webhooks/+page.md new file mode 100644 index 00000000..a957ab74 --- /dev/null +++ b/packages/dashboard/src/routes/(static)/docs/webhooks/+page.md @@ -0,0 +1,262 @@ +# Webhooks + +Webhooks allow you to schedule API calls to your PocketBase instance at specific times, replacing the need for external cron job schedulers. This feature enables automated tasks like data cleanup, backups, notifications, and integrations with external services. + +> **Important**: Webhooks replace PocketBase's built-in [cron job scheduling](https://pocketbase.io/docs/js-jobs-scheduling/) (`cronAdd`) on PocketHost. While `cronAdd` works in standard PocketBase deployments, it becomes unreliable on PocketHost due to instance hibernation. Scheduled webhooks will always execute reliably, even when your instance is hibernated. + +## Overview + +Webhooks are configured through the PocketHost dashboard and automatically execute HTTP requests to your specified endpoints at scheduled intervals. Each webhook consists of: + +- **API Endpoint**: The URL path within your instance to call +- **Schedule**: A cron expression defining when the webhook executes + +All webhooks execute in **UTC time**. Make sure to adjust your cron schedules accordingly. + +### Why Use Webhooks Instead of `cronAdd`? + +On PocketHost, webhooks provide several advantages over PocketBase's built-in `cronAdd`: + +- **Reliability**: Webhooks execute even when your instance is hibernated +- **Consistency**: No dependency on your instance's uptime +- **Scalability**: Handled by PocketHost's infrastructure, not your instance +- **Monitoring**: Better visibility into execution status and failures + +## Configuration + +### API Endpoint + +The API endpoint must be a valid path within your PocketBase instance: + +- Must start with `/` (e.g., `/api/webhooks/backup`) +- Can include query parameters (e.g., `/api/cron?token=abc123`) +- Cannot include protocol or host (no `http://` or `https://`) +- Supports any valid URL path structure + +**Examples:** + +- `/api/webhooks/daily-cleanup` +- `/api/backup?type=full&compress=true` +- `/webhook/slack/notifications` +- `/api/maintenance/cleanup-old-records` + +### Schedule (Cron Expression) + +Webhooks use standard cron expressions to define execution schedules. You can use either: + +#### Predefined Macros + +| Macro | Description | Equivalent Expression | +| ----------- | ------------------------------------ | --------------------- | +| `@yearly` | Once a year at midnight, January 1st | `0 0 1 1 *` | +| `@annually` | Same as `@yearly` | `0 0 1 1 *` | +| `@monthly` | Once a month at midnight, first day | `0 0 1 * *` | +| `@weekly` | Once a week at midnight on Sunday | `0 0 * * 0` | +| `@daily` | Once a day at midnight | `0 0 * * *` | +| `@midnight` | Same as `@daily` | `0 0 * * *` | +| `@hourly` | Once an hour at the beginning | `0 * * * *` | +| `@minutely` | Once a minute | `* * * * *` | +| `@secondly` | Once a second | `* * * * * *` | +| `@weekdays` | Every weekday at midnight | `0 0 * * 1-5` | +| `@weekends` | Every weekend at midnight | `0 0 * * 0,6` | + +#### Standard Cron Expressions + +Standard cron expressions use 5 fields: `minute hour day month weekday` + +| Field | Values | Special Characters | Description | +| ------------ | ------ | ------------------ | -------------------------- | +| Minute | 0-59 | `* , - / ?` | Minute of the hour | +| Hour | 0-23 | `* , - / ?` | Hour of the day | +| Day of Month | 1-31 | `* , - / ? L W` | Day of the month | +| Month | 1-12 | `* , - / ?` | Month of the year | +| Day of Week | 0-6 | `* , - / ? L #` | Day of the week (0=Sunday) | + +**Special Characters:** + +- `*` - Any value +- `,` - Value list separator +- `-` - Range of values +- `/` - Step values +- `?` - Any value (alias for `*`) +- `L` - Last day of month/week +- `W` - Weekday (nearest to given day) +- `#` - Nth day of month + +### Timing + +All webhooks execute in **UTC time**. When scheduling webhooks, convert your local time to UTC: + +- **EST (UTC-5)**: 9 AM EST = 2 PM UTC (14:00) +- **PST (UTC-8)**: 6 PM PST = 2 AM UTC next day (02:00) +- **GMT+3**: 3 PM = 12 PM UTC (12:00) + +Use online UTC converters to help calculate the correct schedule times. + +## Common Examples + +### Business Operations (UTC Time) + +```cron +# Weekdays at 9 AM UTC +0 9 * * 1-5 + +# Every Monday at noon UTC +0 12 * * 1 + +# Every Friday at 6 PM UTC +0 18 * * 5 + +# First day of every month at midnight UTC +0 0 1 * * + +# 15th of every month at 8 AM UTC +0 8 15 * * +``` + +### Data Management (UTC Time) + +```cron +# Daily backup at 2 AM UTC +0 2 * * * + +# Cleanup old records every 6 hours +0 */6 * * * + +# Weekly data export on Sundays at midnight UTC +0 0 * * 0 + +# Monthly maintenance on the 1st at midnight UTC +0 0 1 * * +``` + +### Using Macros + +```cron +# Daily operations +@daily + +# Weekly reports +@weekly + +# Monthly cleanup +@monthly + +# Business hours only +@weekdays +``` + +## Implementation + +### Creating Webhook Endpoints + +Create API endpoints in your PocketBase instance to handle webhook requests using [PocketBase's routing system](https://pocketbase.io/docs/js-routing/): + +```javascript +// pb_hooks/onRequest.pb.js +routerAdd('POST', '/api/webhooks/backup', (e) => { + // Your backup logic here + console.log('Backup webhook triggered') + + // Example: Create a backup record + const backup = new Record($app.dao().findCollectionByNameOrId('backups'), { + timestamp: new Date().toISOString(), + status: 'completed', + size: '1.2GB', + }) + + $app.dao().saveRecord(backup) + + return e.json(200, { status: 'success' }) +}) +``` + +### Error Handling + +Webhooks should return appropriate HTTP status codes: + +- `200` - Success +- `400` - Bad request +- `500` - Internal server error + +```javascript +routerAdd('POST', '/api/webhooks/cleanup', (e) => { + try { + // Your cleanup logic + return e.json(200, { status: 'success' }) + } catch (error) { + console.error('Webhook error:', error) + return e.json(500, { error: 'Internal server error' }) + } +}) +``` + +### Authentication + +For secure webhooks, include authentication in your endpoints: + +```javascript +routerAdd('POST', '/api/webhooks/secure', (e) => { + const token = e.request.url.query().get('token') + + if (token !== process.env.WEBHOOK_SECRET) { + return e.json(401, { error: 'Unauthorized' }) + } + + // Your secure webhook logic + return e.json(200, { status: 'success' }) +}) +``` + +## Best Practices + +### 1. Idempotency + +Make your webhooks idempotent so they can be safely retried: + +```javascript +routerAdd('POST', '/api/webhooks/process', (e) => { + const jobId = e.request.url.query().get('jobId') + + // Check if already processed + const existing = $app.dao().findFirstRecordByData('jobs', 'jobId', jobId) + if (existing && existing.get('status') === 'completed') { + return e.json(200, { status: 'already_processed' }) + } + + // Process the job + // ... +}) +``` + +### 2. Logging + +Always log webhook executions for debugging: + +```javascript +routerAdd('POST', '/api/webhooks/backup', (e) => { + console.log(`Backup webhook triggered at ${new Date().toISOString()}`) + + // Your backup logic + + console.log('Backup webhook completed successfully') + return e.json(200, { status: 'success' }) +}) +``` + +## Troubleshooting + +### Common Issues + +1. **Webhook not executing**: Check the cron expression syntax and ensure times are in UTC +2. **Endpoint not found**: Ensure the API endpoint exists in your PocketBase instance using [PocketBase routing](https://pocketbase.io/docs/js-routing/) +3. **Authentication errors**: Verify any required tokens or secrets +4. **Timeout errors**: Optimize webhook execution time +5. **Using `cronAdd` instead of webhooks**: Replace `cronAdd` calls with scheduled webhooks for reliable execution on PocketHost +6. **Wrong execution time**: Remember all schedules are in UTC - convert your local time accordingly + +## Limitations + +- Concurrent webhook executions may be limited +- Webhooks may not run exactly at the time specified, depending on system load and instance state +- Webhooks are triggered by PocketHost's scheduling system, not your instance's internal clock diff --git a/packages/pockethost/src/common/schema/Instance.ts b/packages/pockethost/src/common/schema/Instance.ts index a2c77cb1..16a81940 100644 --- a/packages/pockethost/src/common/schema/Instance.ts +++ b/packages/pockethost/src/common/schema/Instance.ts @@ -25,6 +25,13 @@ export type InstanceWebhookCollection = InstanceWebhookItem[] export type InstanceWebhookItem = { endpoint: InstanceWebhookEndpoint value: InstanceWebhookValue + lastFired?: { + timestamp: number + response: { + status: number + body: string + } + } } export type InstanceFields = BaseFields & { diff --git a/packages/pockethost/src/mothership-app/pb_hooks/mothership.js b/packages/pockethost/src/mothership-app/pb_hooks/mothership.js index 84534378..a2385b35 100644 --- a/packages/pockethost/src/mothership-app/pb_hooks/mothership.js +++ b/packages/pockethost/src/mothership-app/pb_hooks/mothership.js @@ -251,6 +251,7 @@ const HandleInstanceUpdate = (c) => { power: null, version: null, secrets: null, + webhooks: null, syncAdmin: null, dev: null, cname: null @@ -260,13 +261,14 @@ const HandleInstanceUpdate = (c) => { log(`After bind`); data = JSON.parse(JSON.stringify(data)); const id = c.pathParam("id"); - const { fields: { subdomain, power, version, secrets, syncAdmin, dev, cname } } = data; + const { fields: { subdomain, power, version, secrets, webhooks, syncAdmin, dev, cname } } = data; log(`vars`, JSON.stringify({ id, subdomain, power, version, secrets, + webhooks, syncAdmin, dev, cname @@ -289,6 +291,7 @@ const HandleInstanceUpdate = (c) => { version, power, secrets, + webhooks, syncAdmin, dev, cname diff --git a/packages/pockethost/src/mothership-app/pb_migrations/1753050291_updated_instances.js b/packages/pockethost/src/mothership-app/pb_migrations/1753050291_updated_instances.js new file mode 100644 index 00000000..2a92eb32 --- /dev/null +++ b/packages/pockethost/src/mothership-app/pb_migrations/1753050291_updated_instances.js @@ -0,0 +1,29 @@ +/// +migrate((db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv") + + // add + collection.schema.addField(new SchemaField({ + "system": false, + "id": "5q2bapw9", + "name": "webhooks", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + })) + + return dao.saveCollection(collection) +}, (db) => { + const dao = new Dao(db) + const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv") + + // remove + collection.schema.removeField("5q2bapw9") + + return dao.saveCollection(collection) +}) diff --git a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/api/HandleInstanceUpdate.ts b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/api/HandleInstanceUpdate.ts index dfa6de80..3644fbc4 100644 --- a/packages/pockethost/src/mothership-app/src/lib/handlers/instance/api/HandleInstanceUpdate.ts +++ b/packages/pockethost/src/mothership-app/src/lib/handlers/instance/api/HandleInstanceUpdate.ts @@ -1,3 +1,4 @@ +import { type InstanceWebhookCollection } from '$common' import { mkLog, StringKvLookup } from '$util/Logger' import { removeEmptyKeys } from '$util/removeEmptyKeys' @@ -70,6 +71,7 @@ export const HandleInstanceUpdate = (c: echo.Context) => { power: null, version: null, secrets: null, + webhooks: null, syncAdmin: null, dev: null, cname: null, @@ -81,6 +83,7 @@ export const HandleInstanceUpdate = (c: echo.Context) => { power: boolean | null version: string | null secrets: StringKvLookup | null + webhooks: InstanceWebhookCollection | null syncAdmin: boolean | null dev: boolean | null cname: string | null @@ -95,7 +98,7 @@ export const HandleInstanceUpdate = (c: echo.Context) => { const id = c.pathParam('id') const { - fields: { subdomain, power, version, secrets, syncAdmin, dev, cname }, + fields: { subdomain, power, version, secrets, webhooks, syncAdmin, dev, cname }, } = data log( @@ -106,6 +109,7 @@ export const HandleInstanceUpdate = (c: echo.Context) => { power, version, secrets, + webhooks, syncAdmin, dev, cname, @@ -143,6 +147,7 @@ export const HandleInstanceUpdate = (c: echo.Context) => { version, power, secrets, + webhooks, syncAdmin, dev, cname, diff --git a/packages/pockethost/src/mothership-app/tsconfig.json b/packages/pockethost/src/mothership-app/tsconfig.json index d68138f5..23a00791 100644 --- a/packages/pockethost/src/mothership-app/tsconfig.json +++ b/packages/pockethost/src/mothership-app/tsconfig.json @@ -9,6 +9,7 @@ "moduleResolution": "node", "baseUrl": ".", "paths": { + "$common": ["../common/index.ts"], "$util/*": ["src/lib/util/*"] } }, diff --git a/packages/pockethost/src/services/CronService/index.ts b/packages/pockethost/src/services/CronService/index.ts new file mode 100644 index 00000000..7488ba53 --- /dev/null +++ b/packages/pockethost/src/services/CronService/index.ts @@ -0,0 +1,118 @@ +import { + InstanceFields, + InstanceId, + LoggerService, + mkInstanceUrl, + mkSingleton, + MothershipAdminClientService, + SingletonBaseConfig, +} from '@' +import Bottleneck from 'bottleneck' +import { CronJob } from 'cron' +import { MothershipMirrorService } from '../MothershipMirrorService' + +export type CronServiceConfig = SingletonBaseConfig & {} + +export const CronService = mkSingleton(async (config: Partial) => { + const mirror = await MothershipMirrorService() + const logger = (config.logger ?? LoggerService()).create(`CronService`) + const { dbg, error, info, warn } = logger + info(`Starting`) + + const { client } = await MothershipAdminClientService() + + const limiter = new Bottleneck({ maxConcurrent: 10 }) + const jobs: Map> = new Map() + + const removeJobsForInstanceId = (instanceId: InstanceId) => { + if (jobs.has(instanceId)) { + dbg(`Stopping jobs for instance ${instanceId}`) + jobs.get(instanceId)?.forEach((job) => { + job.stop() + }) + dbg(`Deleted jobs for instance ${instanceId}`) + jobs.delete(instanceId) + } + } + + const upsertInstance = async (instance: InstanceFields) => { + removeJobsForInstanceId(instance.id) + const newJobs = new Set() + const { webhooks } = instance + if (!webhooks) { + dbg(`Instance ${instance.id} has no webhooks`) + return + } + dbg(`Instance has ${instance.webhooks?.length} webhooks`) + dbg(`Instance has ${jobs.get(instance.id)?.size} jobs`) + dbg(`Creating new jobs for instance ${instance.id}`) + webhooks.forEach((webhook) => { + if (!webhook.value) return + dbg(`Creating new job for webhook ${webhook.endpoint} (${webhook.value}) for instance ${instance.id}`) + const job = new CronJob( + webhook.value, // cronTime + () => { + dbg(`Firing webhook ${webhook.endpoint} (${webhook.value}) for instance ${instance.id}`) + limiter.schedule(async () => { + const url = mkInstanceUrl(instance, ...webhook.endpoint.split('/').filter(Boolean)) + dbg(`Firing webhook ${url}`) + try { + const response = await fetch(url, {}) + const body = await response.text() + + // fetch only throws for network errors, not HTTP error status codes + // so 404, 500, etc. will be handled here without throwing + webhook.lastFired = { + timestamp: Date.now(), + response: { + status: response.status, + body: body, + }, + } + + // Optionally log non-2xx responses + if (!response.ok) { + warn(`Webhook ${url} returned status ${response.status}: ${body}`) + } + } catch (e) { + // This catch block only handles network errors (connection failed, etc.) + webhook.lastFired = { + timestamp: Date.now(), + response: { + status: 500, + body: e instanceof Error ? e.message : String(e), + }, + } + error(`Network error firing webhook ${url}: ${e}`) + } + dbg(`Updating instance ${instance.id} with webhooks`, instance.webhooks) + client.updateInstance(instance.id, { + webhooks: instance.webhooks, + }) + }) + } + ) + job.start() + newJobs.add(job) + }) + jobs.set(instance.id, newJobs) + dbg(`Created ${newJobs.size} jobs for instance ${instance.id}`) + } + + mirror.onInstanceUpserted((instance) => { + dbg(`Instance upserted: ${instance.id}`) + upsertInstance(instance) + }) + + mirror.onInstanceDeleted((instanceId) => { + dbg(`Instance deleted: ${instanceId}`) + removeJobsForInstanceId(instanceId) + }) + + dbg(`Upserting instances`) + for (const instance of mirror.getInstances()) { + upsertInstance(instance) + } + + return {} +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff9a7176..37dcf55c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) + cron-parser: + specifier: ^5.3.0 + version: 5.3.0 d3-scale: specifier: ^4.0.2 version: 4.0.2 @@ -175,6 +178,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + cron: + specifier: ^4.3.2 + version: 4.3.2 devcert: specifier: ^1.2.2 version: 1.2.2 @@ -256,12 +262,6 @@ importers: vhost: specifier: ^3.0.2 version: 3.0.2 - winston: - specifier: ^3.17.0 - version: 3.17.0 - winston-transport: - specifier: ^4.9.0 - version: 4.9.0 devDependencies: '@types/better-sqlite3': specifier: ^7.6.12 @@ -477,17 +477,10 @@ packages: cpu: [x64] os: [win32] - '@colors/colors@1.6.0': - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@dabh/diagnostics@2.0.3': - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - '@emnapi/core@1.4.4': resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==} @@ -1743,6 +1736,9 @@ packages: '@types/lodash@4.17.14': resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + '@types/luxon@3.6.2': + resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1803,9 +1799,6 @@ packages: '@types/tmp@0.0.33': resolution: {integrity: sha512-gVC1InwyVrO326wbBZw+AO3u2vRXz/iRWq9jYhpG4W8LXyIgDv3ZmcLQ5Q4Gs+gFMyqx+viFoFT+l3p61QFCmQ==} - '@types/triple-beam@1.3.5': - resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1916,9 +1909,6 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -2091,32 +2081,20 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} - color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} - command-exists@1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} @@ -2179,6 +2157,14 @@ packages: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} + cron-parser@5.3.0: + resolution: {integrity: sha512-IS4mnFu6n3CFgEmXjr+B2zzGHsjJmHEdN+BViKvYSiEn3KWss9ICRDETDX/VZldiW82B94OyAZm4LIT4vcKK0g==} + engines: {node: '>=18'} + + cron@4.3.2: + resolution: {integrity: sha512-JxBBnf5zRz+NhW9XcP16gwUKAKIimy2G0QCCQu8kk5XwM4aCGwMt+nntouAfXF9A57965XzB6hitBlJAz5Ts6w==} + engines: {node: '>=18.x'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2422,9 +2408,6 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} - enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -2585,9 +2568,6 @@ packages: picomatch: optional: true - fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -2631,9 +2611,6 @@ packages: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} - fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -2903,10 +2880,6 @@ packages: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-stream@4.0.1: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} @@ -2984,9 +2957,6 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3026,10 +2996,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - logform@2.7.0: - resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} - engines: {node: '>= 12.0.0'} - long@5.2.4: resolution: {integrity: sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==} @@ -3040,6 +3006,10 @@ packages: resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} engines: {node: 20 || >=22} + luxon@3.7.1: + resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + engines: {node: '>=12'} + lzma-native@8.0.6: resolution: {integrity: sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==} engines: {node: '>=10.0.0'} @@ -3316,9 +3286,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} - os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -3748,10 +3715,6 @@ packages: safe-json-stringify@1.2.0: resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3868,9 +3831,6 @@ packages: resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==} engines: {node: '>=10.16.0'} - stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -4031,9 +3991,6 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4081,10 +4038,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -4319,14 +4272,6 @@ packages: engines: {node: '>= 8'} hasBin: true - winston-transport@4.9.0: - resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} - engines: {node: '>= 12.0.0'} - - winston@3.17.0: - resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} - engines: {node: '>= 12.0.0'} - with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -4635,18 +4580,10 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250712.0': optional: true - '@colors/colors@1.6.0': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@dabh/diagnostics@2.0.3': - dependencies: - colorspace: 1.1.4 - enabled: 2.0.0 - kuler: 2.0.0 - '@emnapi/core@1.4.4': dependencies: '@emnapi/wasi-threads': 1.0.3 @@ -5586,6 +5523,8 @@ snapshots: '@types/lodash@4.17.14': {} + '@types/luxon@3.6.2': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 2.0.11 @@ -5648,8 +5587,6 @@ snapshots: '@types/tmp@0.0.33': {} - '@types/triple-beam@1.3.5': {} - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -5739,8 +5676,6 @@ snapshots: astral-regex@2.0.0: {} - async@3.2.6: {} - autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.1 @@ -5939,16 +5874,10 @@ snapshots: clsx@2.1.1: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} color-string@1.9.1: @@ -5956,21 +5885,11 @@ snapshots: color-name: 1.1.4 simple-swizzle: 0.2.2 - color@3.2.1: - dependencies: - color-convert: 1.9.3 - color-string: 1.9.1 - color@4.2.3: dependencies: color-convert: 2.0.1 color-string: 1.9.1 - colorspace@1.1.4: - dependencies: - color: 3.2.1 - text-hex: 1.0.0 - command-exists@1.2.9: {} commander@12.1.0: {} @@ -6018,6 +5937,15 @@ snapshots: nan: 2.22.0 optional: true + cron-parser@5.3.0: + dependencies: + luxon: 3.7.1 + + cron@4.3.2: + dependencies: + '@types/luxon': 3.6.2 + luxon: 3.7.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6266,8 +6194,6 @@ snapshots: empathic@2.0.0: {} - enabled@2.0.0: {} - encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -6494,8 +6420,6 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - fecha@4.2.3: {} - fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -6545,8 +6469,6 @@ snapshots: path-exists: 5.0.0 unicorn-magic: 0.1.0 - fn.name@1.1.0: {} - follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: debug: 4.4.0 @@ -6870,8 +6792,6 @@ snapshots: is-stream@1.1.0: {} - is-stream@2.0.1: {} - is-stream@4.0.1: {} is-subdir@1.2.0: @@ -6940,8 +6860,6 @@ snapshots: kleur@4.1.5: {} - kuler@2.0.0: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -6970,21 +6888,14 @@ snapshots: lodash@4.17.21: {} - logform@2.7.0: - dependencies: - '@colors/colors': 1.6.0 - '@types/triple-beam': 1.3.5 - fecha: 4.2.3 - ms: 2.1.3 - safe-stable-stringify: 2.5.0 - triple-beam: 1.4.1 - long@5.2.4: {} lru-cache@10.4.3: {} lru-cache@11.0.2: {} + luxon@3.7.1: {} + lzma-native@8.0.6: dependencies: node-addon-api: 3.2.1 @@ -7318,10 +7229,6 @@ snapshots: dependencies: wrappy: 1.0.2 - one-time@1.0.0: - dependencies: - fn.name: 1.1.0 - os-tmpdir@1.0.2: {} outdent@0.5.0: {} @@ -7794,8 +7701,6 @@ snapshots: safe-json-stringify@1.2.0: optional: true - safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: {} sass@1.89.2: @@ -7984,8 +7889,6 @@ snapshots: cpu-features: 0.0.10 nan: 2.22.0 - stack-trace@0.0.10: {} - statuses@2.0.1: {} stoppable@1.1.0: {} @@ -8175,8 +8078,6 @@ snapshots: term-size@2.2.1: {} - text-hex@1.0.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -8218,8 +8119,6 @@ snapshots: totalist@3.0.1: {} - triple-beam@1.4.1: {} - ts-interface-checker@0.1.13: {} tsdown@0.12.9(typescript@5.7.3): @@ -8418,26 +8317,6 @@ snapshots: dependencies: isexe: 2.0.0 - winston-transport@4.9.0: - dependencies: - logform: 2.7.0 - readable-stream: 3.6.2 - triple-beam: 1.4.1 - - winston@3.17.0: - dependencies: - '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.3 - async: 3.2.6 - is-stream: 2.0.1 - logform: 2.7.0 - one-time: 1.0.0 - readable-stream: 3.6.2 - safe-stable-stringify: 2.5.0 - stack-trace: 0.0.10 - triple-beam: 1.4.1 - winston-transport: 4.9.0 - with@7.0.2: dependencies: '@babel/parser': 7.28.0