Convert JSON storage to SQLite #3
@@ -8,7 +8,8 @@ PORT=3003
|
||||
# If unset, uses Application Default Credentials (e.g. GOOGLE_APPLICATION_CREDENTIALS).
|
||||
# FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account",...}
|
||||
|
||||
# Local persistence directory for registered FCM tokens (default: ./data)
|
||||
# Directory for the SQLite FCM registration database (default: ./data).
|
||||
# Creates notify.sqlite (plus -wal/-shm while the process is running).
|
||||
# FCM_TOKEN_DATA_DIR=./data
|
||||
|
||||
# Set to "test-local" to bypass ethr JWT expiry verification in local dev only.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# ---- build stage: install everything, type-check + compile to JS ----
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
@@ -15,6 +17,8 @@ RUN pnpm build
|
||||
# ---- runtime stage: prod deps + compiled JS only, no tsx/esbuild ----
|
||||
FROM node:22-alpine AS runtime
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
52
README.md
52
README.md
@@ -1,5 +1,7 @@
|
||||
A lightweight Express service that schedules and sends Firebase Cloud Messaging (FCM) push notifications to wake up registered devices.
|
||||
|
||||
Device registrations are stored in a local **SQLite** database (not JSON).
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
@@ -11,6 +13,8 @@ Here is one way to generate the contents: `cat your-downloaded-key.json | jq -c
|
||||
|
||||
Optionally set `ENDORSER_URL` if you are not using the default production Endorser API (`https://api.endorser.ch`).
|
||||
|
||||
Optionally set `FCM_TOKEN_DATA_DIR` if you want the SQLite database somewhere other than `./data`.
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
@@ -18,6 +22,8 @@ pnpm run dev
|
||||
|
||||
The server starts on `http://localhost:3003` (or the port in `PORT`). Hot-reloads on file changes.
|
||||
|
||||
On first use, the service creates `FCM_TOKEN_DATA_DIR` (default `./data`) and the SQLite file `notify.sqlite` with the required schema.
|
||||
|
||||
### Authentication
|
||||
|
||||
`POST /notifications/register` and `POST /notifications/refresh` require a Bearer JWT. After local JWT verification, the service checks the token with Endorser (`GET /api/report/rateLimits` on `ENDORSER_URL`). Registration and refresh continue only if Endorser accepts the JWT.
|
||||
@@ -26,6 +32,42 @@ The server starts on `http://localhost:3003` (or the port in `PORT`). Hot-reload
|
||||
|
||||
Set `NODE_ENV=test-local` in `.env` to bypass ethr JWT *expiry* verification during local development (this is separate from the `testMode` bypass above).
|
||||
|
||||
## Storage
|
||||
|
||||
### Database location
|
||||
|
||||
| Path | Description |
|
||||
|---|---|
|
||||
| `{FCM_TOKEN_DATA_DIR}/notify.sqlite` | Primary SQLite database (default dir: `./data`) |
|
||||
| `{FCM_TOKEN_DATA_DIR}/notify.sqlite-wal` | WAL journal (present while the process is running) |
|
||||
| `{FCM_TOKEN_DATA_DIR}/notify.sqlite-shm` | Shared-memory file used with WAL mode |
|
||||
|
||||
`FCM_TOKEN_DATA_DIR` defaults to `./data` (relative to the process working directory). The `data/` directory is gitignored.
|
||||
|
||||
### Schema (high level)
|
||||
|
||||
Table `fcm_registrations` holds one row per registered device:
|
||||
|
||||
- Identity: `id`, `user_id`, `device_id`, `fcm_token`, `platform`
|
||||
- Flags: `test_mode`
|
||||
- Timestamps: `created_at`, `updated_at`, `last_notified_at`
|
||||
|
||||
Unique on `(user_id, device_id)`. Indexes also exist on `user_id`, `device_id`, `fcm_token`, and `(user_id, fcm_token)`.
|
||||
|
||||
The schema is created automatically on startup if the database or tables do not already exist.
|
||||
|
||||
### JSON → SQLite
|
||||
|
||||
There is **no automatic migration** from the old JSON file (`fcm-tokens.json`). That format is no longer used. If you still have a local `fcm-tokens.json` from earlier development, it is ignored; re-register devices or import data manually if you need it.
|
||||
|
||||
### Backup
|
||||
|
||||
Persist or back up the SQLite files under `FCM_TOKEN_DATA_DIR`:
|
||||
|
||||
1. Prefer stopping the service, then copy `notify.sqlite` (and any `-wal` / `-shm` sidecars if present).
|
||||
2. Or, while the service is running, copy **all three** files (`notify.sqlite`, `-wal`, `-shm`) together so the backup stays consistent under WAL mode.
|
||||
3. For Docker, mount a volume at the data directory (or set `FCM_TOKEN_DATA_DIR` to a mounted path) so registrations survive container recreation.
|
||||
|
||||
## Production
|
||||
|
||||
Runs TypeScript directly via `tsx` (no compile step).
|
||||
@@ -35,13 +77,19 @@ pnpm install --prod
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Ensure `FCM_TOKEN_DATA_DIR` points at a durable location (or accept the default `./data` next to the process cwd).
|
||||
|
||||
Or with Docker:
|
||||
|
||||
```bash
|
||||
docker build --no-cache -t notify-wakeup-api:amd-$NOTIFY_WAKEUP_API_VERSION --platform linux/amd64 .
|
||||
docker run --env-file notify-wakeup-api.env -p 3003:3003 notify-wakeup-api
|
||||
docker run --env-file notify-wakeup-api.env -p 3003:3003 \
|
||||
-v notify-wakeup-data:/app/data \
|
||||
notify-wakeup-api
|
||||
```
|
||||
|
||||
Mount a volume over `/app/data` (or whatever path you set with `FCM_TOKEN_DATA_DIR`) so the SQLite database is not lost when the container is replaced.
|
||||
|
||||
Required environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
@@ -49,4 +97,4 @@ Required environment variables:
|
||||
| `FIREBASE_SERVICE_ACCOUNT_JSON` | Inline service account JSON (one line). If unset, falls back to Application Default Credentials. |
|
||||
| `PORT` | HTTP port (default: `3003`). |
|
||||
| `ENDORSER_URL` | Endorser API base URL used for auth checks on register/refresh (default: `https://api.endorser.ch`). |
|
||||
| `FCM_TOKEN_DATA_DIR` | Directory for persisting registered FCM tokens (default: `./data`). |
|
||||
| `FCM_TOKEN_DATA_DIR` | Directory for the SQLite database file `notify.sqlite` (default: `./data`). |
|
||||
|
||||
289
package-lock.json
generated
289
package-lock.json
generated
@@ -1,29 +1,30 @@
|
||||
{
|
||||
"name": "notification-wakeup-service",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "notification-wakeup-service",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-ecc": "^2.7.0",
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"better-sqlite3": "^13.0.1",
|
||||
"cbor-x": "^1.6.4",
|
||||
"cors": "^2.8.6",
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.2.1",
|
||||
"firebase-admin": "^13.10.0",
|
||||
"tsx": "^4.22.3"
|
||||
"firebase-admin": "^13.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.19.19",
|
||||
"tsx": "^4.19.2",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
@@ -106,9 +107,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -123,9 +124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -140,9 +141,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -157,9 +158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -174,9 +175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -191,9 +192,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -208,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -225,9 +226,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -242,9 +243,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -259,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -276,9 +277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -293,9 +294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -310,9 +311,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -327,9 +328,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -344,9 +345,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -361,9 +362,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -378,9 +379,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -395,9 +396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -412,9 +413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -429,9 +430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -446,9 +447,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -463,9 +464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -480,9 +481,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -497,9 +498,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -514,9 +515,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -531,9 +532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1054,6 +1055,16 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -1333,6 +1344,19 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.1.tgz",
|
||||
"integrity": "sha512-LYpmOXdkpQYf4wmlxkdzW01XGlOXNIbjLg45yNkh0FQ4814VbK9PdOFmhZpYbej+EZtR/i3FDdhEG98HqZdgnA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
@@ -1760,9 +1784,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1773,32 +1797,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -2245,19 +2269,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.6.2",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
|
||||
@@ -2877,6 +2888,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz",
|
||||
"integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -3169,16 +3189,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
@@ -3547,14 +3557,13 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"version": "4.23.1",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
|
||||
"integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
|
||||
@@ -12,18 +12,20 @@
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-ecc": "^2.7.0",
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"better-sqlite3": "^13.0.1",
|
||||
"cbor-x": "^1.6.4",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.2.1",
|
||||
"firebase-admin": "^13.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.19.19",
|
||||
"@types/cors": "^2.8.19",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@peculiar/asn1-schema':
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
better-sqlite3:
|
||||
specifier: ^13.0.1
|
||||
version: 13.0.1
|
||||
cbor-x:
|
||||
specifier: ^1.6.4
|
||||
version: 1.6.4
|
||||
@@ -36,6 +39,9 @@ importers:
|
||||
specifier: ^13.10.0
|
||||
version: 13.10.0
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.13
|
||||
version: 7.6.13
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
@@ -382,6 +388,9 @@ packages:
|
||||
resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
@@ -474,6 +483,10 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
better-sqlite3@13.0.1:
|
||||
resolution: {integrity: sha512-LYpmOXdkpQYf4wmlxkdzW01XGlOXNIbjLg45yNkh0FQ4814VbK9PdOFmhZpYbej+EZtR/i3FDdhEG98HqZdgnA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
@@ -924,6 +937,10 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-addon-api@8.9.0:
|
||||
resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@@ -1492,6 +1509,10 @@ snapshots:
|
||||
'@tootallnate/once@2.0.1':
|
||||
optional: true
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
dependencies:
|
||||
'@types/node': 22.19.19
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -1607,6 +1628,10 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-sqlite3@13.0.1:
|
||||
dependencies:
|
||||
node-addon-api: 8.9.0
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
@@ -2210,6 +2235,8 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-addon-api@8.9.0: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
strictDepBuilds: false
|
||||
onlyBuiltDependencies:
|
||||
- "@firebase/util"
|
||||
- better-sqlite3
|
||||
- cbor-extract
|
||||
- esbuild
|
||||
- protobufjs
|
||||
allowBuilds:
|
||||
'@firebase/util': set this to true or false
|
||||
better-sqlite3: set this to true or false
|
||||
cbor-extract: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
protobufjs: set this to true or false
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const dataDir =
|
||||
process.env.FCM_TOKEN_DATA_DIR ?? path.join(process.cwd(), "data");
|
||||
const dataFile = path.join(dataDir, "fcm-tokens.json");
|
||||
|
||||
export type StoredRow = {
|
||||
id: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
fcmToken: string;
|
||||
platform: string;
|
||||
testMode?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastNotifiedAt?: number;
|
||||
};
|
||||
|
||||
type ParsedRow = {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
fcmToken: string;
|
||||
platform: string;
|
||||
testMode?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastNotifiedAt?: number | string;
|
||||
};
|
||||
|
||||
export function storageKey(userId: string, deviceId: string): string {
|
||||
return `${userId}::${deviceId}`;
|
||||
}
|
||||
|
||||
function mergeDeviceRows(
|
||||
key: string,
|
||||
a: StoredRow,
|
||||
b: StoredRow
|
||||
): StoredRow {
|
||||
const primary =
|
||||
new Date(a.updatedAt) >= new Date(b.updatedAt) ? a : b;
|
||||
const lastMs = Math.max(a.lastNotifiedAt ?? 0, b.lastNotifiedAt ?? 0);
|
||||
const created =
|
||||
new Date(a.createdAt) <= new Date(b.createdAt)
|
||||
? a.createdAt
|
||||
: b.createdAt;
|
||||
return {
|
||||
...primary,
|
||||
id: primary.id,
|
||||
userId: primary.userId,
|
||||
deviceId: primary.deviceId,
|
||||
fcmToken: primary.fcmToken,
|
||||
lastNotifiedAt: lastMs > 0 ? lastMs : undefined,
|
||||
createdAt: created,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeParsedRow(
|
||||
mapKey: string,
|
||||
r: ParsedRow,
|
||||
onMutate: () => void
|
||||
): StoredRow {
|
||||
let id = r.id;
|
||||
if (id === undefined || id === "") {
|
||||
id = randomUUID();
|
||||
onMutate();
|
||||
}
|
||||
|
||||
let lastNotifiedAt: number | undefined;
|
||||
if (typeof r.lastNotifiedAt === "string") {
|
||||
const ms = Date.parse(r.lastNotifiedAt);
|
||||
lastNotifiedAt = Number.isNaN(ms) ? undefined : ms;
|
||||
onMutate();
|
||||
} else if (typeof r.lastNotifiedAt === "number") {
|
||||
lastNotifiedAt = Number.isNaN(r.lastNotifiedAt)
|
||||
? undefined
|
||||
: r.lastNotifiedAt;
|
||||
}
|
||||
|
||||
const deviceId = (r.deviceId ?? r.fcmToken ?? mapKey).trim();
|
||||
if (r.deviceId === undefined || r.deviceId === "") {
|
||||
onMutate();
|
||||
}
|
||||
|
||||
let userId = r.userId?.trim();
|
||||
if (userId === undefined || userId === "") {
|
||||
const fromKey = mapKey.includes("::")
|
||||
? mapKey.slice(0, mapKey.indexOf("::"))
|
||||
: "";
|
||||
userId = fromKey || "__legacy__";
|
||||
onMutate();
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
deviceId,
|
||||
fcmToken: r.fcmToken,
|
||||
platform: r.platform,
|
||||
testMode: r.testMode,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
lastNotifiedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function rowKey(row: StoredRow): string {
|
||||
if (row.userId === "__legacy__") {
|
||||
return row.deviceId;
|
||||
}
|
||||
return storageKey(row.userId, row.deviceId);
|
||||
}
|
||||
|
||||
async function load(): Promise<Record<string, StoredRow>> {
|
||||
try {
|
||||
const raw = await readFile(dataFile, "utf8");
|
||||
const parsed = JSON.parse(raw) as Record<string, ParsedRow>;
|
||||
let dirty = false;
|
||||
const markDirty = (): void => {
|
||||
dirty = true;
|
||||
};
|
||||
|
||||
const buckets = new Map<string, StoredRow[]>();
|
||||
|
||||
for (const [mapKey, rawRow] of Object.entries(parsed)) {
|
||||
const row = normalizeParsedRow(mapKey, rawRow, markDirty);
|
||||
const key = rowKey(row);
|
||||
if (mapKey !== key) markDirty();
|
||||
const list = buckets.get(key) ?? [];
|
||||
list.push(row);
|
||||
buckets.set(key, list);
|
||||
}
|
||||
|
||||
const out: Record<string, StoredRow> = {};
|
||||
for (const [key, rows] of buckets) {
|
||||
if (rows.length === 1) {
|
||||
out[key] = rows[0];
|
||||
} else {
|
||||
out[key] = rows
|
||||
.slice(1)
|
||||
.reduce((acc, cur) => mergeDeviceRows(key, acc, cur), rows[0]);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) await save(out);
|
||||
return out;
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException).code;
|
||||
if (code === "ENOENT") return {};
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function save(records: Record<string, StoredRow>): Promise<void> {
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
const tmp = path.join(dataDir, `.fcm-tokens.${process.pid}.tmp`);
|
||||
const payload = JSON.stringify(records, null, 2);
|
||||
await writeFile(tmp, payload, "utf8");
|
||||
await rename(tmp, dataFile);
|
||||
}
|
||||
|
||||
export const db = {
|
||||
async upsert(row: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
fcmToken: string;
|
||||
platform: string;
|
||||
testMode?: boolean;
|
||||
updatedAt: Date;
|
||||
}): Promise<void> {
|
||||
const all = await load();
|
||||
const key = storageKey(row.userId, row.deviceId);
|
||||
const prev = all[key];
|
||||
const now = row.updatedAt.toISOString();
|
||||
all[key] = {
|
||||
id: prev?.id ?? randomUUID(),
|
||||
userId: row.userId,
|
||||
deviceId: row.deviceId,
|
||||
fcmToken: row.fcmToken,
|
||||
platform: row.platform,
|
||||
testMode: row.testMode,
|
||||
updatedAt: now,
|
||||
createdAt: prev?.createdAt ?? now,
|
||||
lastNotifiedAt: prev?.lastNotifiedAt,
|
||||
};
|
||||
|
||||
for (const k of [...Object.keys(all)]) {
|
||||
const other = all[k];
|
||||
if (
|
||||
k !== key &&
|
||||
other.userId === row.userId &&
|
||||
other.fcmToken === row.fcmToken
|
||||
) {
|
||||
delete all[k];
|
||||
}
|
||||
}
|
||||
|
||||
await save(all);
|
||||
},
|
||||
|
||||
async getAll(): Promise<StoredRow[]> {
|
||||
const all = await load();
|
||||
return Object.values(all);
|
||||
},
|
||||
|
||||
/** Scheduler iteration; excludes legacy rows pending migration cleanup. */
|
||||
async getAllForScheduler(): Promise<StoredRow[]> {
|
||||
const all = await load();
|
||||
// TODO: migrate or remove __legacy__ rows after auth rollout
|
||||
return Object.values(all).filter((r) => r.userId !== "__legacy__");
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve a device owned by userId via deviceId and/or fcmToken.
|
||||
* When both are given, they must refer to the same row.
|
||||
*/
|
||||
async resolveOwnedDevice(
|
||||
userId: string,
|
||||
query: { deviceId?: string; fcmToken?: string }
|
||||
): Promise<StoredRow | undefined> {
|
||||
const deviceId = query.deviceId?.trim();
|
||||
const fcmToken = query.fcmToken;
|
||||
|
||||
if (deviceId !== undefined && deviceId.length > 0) {
|
||||
const byDevice = await this.getByDeviceId(userId, deviceId);
|
||||
if (byDevice === undefined) return undefined;
|
||||
if (
|
||||
fcmToken !== undefined &&
|
||||
fcmToken.length > 0 &&
|
||||
byDevice.fcmToken !== fcmToken
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return byDevice;
|
||||
}
|
||||
|
||||
if (fcmToken !== undefined && fcmToken.length > 0) {
|
||||
return this.getByFcmTokenForUser(userId, fcmToken);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async getByUserId(userId: string): Promise<StoredRow[]> {
|
||||
const all = await load();
|
||||
return Object.values(all).filter((r) => r.userId === userId);
|
||||
},
|
||||
|
||||
async getByDeviceId(
|
||||
userId: string,
|
||||
deviceId: string
|
||||
): Promise<StoredRow | undefined> {
|
||||
const all = await load();
|
||||
return all[storageKey(userId, deviceId)];
|
||||
},
|
||||
|
||||
async getByFcmToken(fcmToken: string): Promise<StoredRow | undefined> {
|
||||
const all = await load();
|
||||
const matches = Object.values(all).filter((r) => r.fcmToken === fcmToken);
|
||||
if (matches.length === 0) return undefined;
|
||||
const owned = matches.filter((r) => r.userId !== "__legacy__");
|
||||
const pool = owned.length > 0 ? owned : matches;
|
||||
return pool.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
)[0];
|
||||
},
|
||||
|
||||
async getByFcmTokenForUser(
|
||||
userId: string,
|
||||
fcmToken: string
|
||||
): Promise<StoredRow | undefined> {
|
||||
const all = await load();
|
||||
return Object.values(all).find(
|
||||
(r) => r.userId === userId && r.fcmToken === fcmToken
|
||||
);
|
||||
},
|
||||
|
||||
async update(id: string, patch: { lastNotifiedAt: number }): Promise<void> {
|
||||
const all = await load();
|
||||
const found = Object.entries(all).find(([, r]) => r.id === id);
|
||||
if (found === undefined) return;
|
||||
const [key, row] = found;
|
||||
all[key] = { ...row, ...patch };
|
||||
await save(all);
|
||||
},
|
||||
};
|
||||
219
src/db/fcmTokensSqlite.ts
Normal file
219
src/db/fcmTokensSqlite.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getDatabase } from "./sqlite.js";
|
||||
|
||||
/**
|
||||
* SQLite-backed FCM registration repository.
|
||||
* This is the service's storage backend.
|
||||
*/
|
||||
export type StoredRow = {
|
||||
id: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
fcmToken: string;
|
||||
platform: string;
|
||||
testMode?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastNotifiedAt?: number;
|
||||
};
|
||||
|
||||
type DbRow = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
fcm_token: string;
|
||||
platform: string;
|
||||
test_mode: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_notified_at: number | null;
|
||||
};
|
||||
|
||||
function toStoredRow(row: DbRow): StoredRow {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
deviceId: row.device_id,
|
||||
fcmToken: row.fcm_token,
|
||||
platform: row.platform,
|
||||
testMode: row.test_mode === null ? undefined : row.test_mode !== 0,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
lastNotifiedAt:
|
||||
row.last_notified_at === null ? undefined : row.last_notified_at,
|
||||
};
|
||||
}
|
||||
|
||||
function testModeToDb(testMode: boolean | undefined): number | null {
|
||||
if (testMode === undefined) return null;
|
||||
return testMode ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Columns required to build a StoredRow. */
|
||||
const ROW_COLUMNS =
|
||||
"id, user_id, device_id, fcm_token, platform, test_mode, created_at, updated_at, last_notified_at";
|
||||
|
||||
export const db = {
|
||||
async upsert(row: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
fcmToken: string;
|
||||
platform: string;
|
||||
testMode?: boolean;
|
||||
updatedAt: Date;
|
||||
}): Promise<void> {
|
||||
const connection = getDatabase();
|
||||
const now = row.updatedAt.toISOString();
|
||||
|
||||
// No pre-read: ON CONFLICT preserves id, created_at, and last_notified_at.
|
||||
const run = connection.transaction(() => {
|
||||
connection
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO fcm_registrations (
|
||||
id, user_id, device_id, fcm_token, platform, test_mode,
|
||||
created_at, updated_at, last_notified_at
|
||||
) VALUES (
|
||||
@id, @user_id, @device_id, @fcm_token, @platform, @test_mode,
|
||||
@created_at, @updated_at, @last_notified_at
|
||||
)
|
||||
ON CONFLICT(user_id, device_id) DO UPDATE SET
|
||||
fcm_token = excluded.fcm_token,
|
||||
platform = excluded.platform,
|
||||
test_mode = excluded.test_mode,
|
||||
updated_at = excluded.updated_at
|
||||
`
|
||||
)
|
||||
.run({
|
||||
id: randomUUID(),
|
||||
user_id: row.userId,
|
||||
device_id: row.deviceId,
|
||||
fcm_token: row.fcmToken,
|
||||
platform: row.platform,
|
||||
test_mode: testModeToDb(row.testMode),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_notified_at: null,
|
||||
});
|
||||
|
||||
connection
|
||||
.prepare(
|
||||
`
|
||||
DELETE FROM fcm_registrations
|
||||
WHERE user_id = ? AND fcm_token = ? AND device_id != ?
|
||||
`
|
||||
)
|
||||
.run(row.userId, row.fcmToken, row.deviceId);
|
||||
});
|
||||
|
||||
run();
|
||||
},
|
||||
|
||||
async getAll(): Promise<StoredRow[]> {
|
||||
const rows = getDatabase()
|
||||
.prepare(`SELECT ${ROW_COLUMNS} FROM fcm_registrations`)
|
||||
.all() as DbRow[];
|
||||
return rows.map(toStoredRow);
|
||||
},
|
||||
|
||||
/** Scheduler iteration; excludes `__legacy__` rows. */
|
||||
async getAllForScheduler(): Promise<StoredRow[]> {
|
||||
const rows = getDatabase()
|
||||
.prepare(
|
||||
`SELECT ${ROW_COLUMNS} FROM fcm_registrations WHERE user_id != '__legacy__'`
|
||||
)
|
||||
.all() as DbRow[];
|
||||
return rows.map(toStoredRow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve a device owned by userId via deviceId and/or fcmToken.
|
||||
* When both are given, they must refer to the same row.
|
||||
*/
|
||||
async resolveOwnedDevice(
|
||||
userId: string,
|
||||
query: { deviceId?: string; fcmToken?: string }
|
||||
): Promise<StoredRow | undefined> {
|
||||
const deviceId = query.deviceId?.trim();
|
||||
const fcmToken = query.fcmToken;
|
||||
|
||||
if (deviceId !== undefined && deviceId.length > 0) {
|
||||
const byDevice = await this.getByDeviceId(userId, deviceId);
|
||||
if (byDevice === undefined) return undefined;
|
||||
if (
|
||||
fcmToken !== undefined &&
|
||||
fcmToken.length > 0 &&
|
||||
byDevice.fcmToken !== fcmToken
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return byDevice;
|
||||
}
|
||||
|
||||
if (fcmToken !== undefined && fcmToken.length > 0) {
|
||||
return this.getByFcmTokenForUser(userId, fcmToken);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async getByUserId(userId: string): Promise<StoredRow[]> {
|
||||
const rows = getDatabase()
|
||||
.prepare(
|
||||
`SELECT ${ROW_COLUMNS} FROM fcm_registrations WHERE user_id = ?`
|
||||
)
|
||||
.all(userId) as DbRow[];
|
||||
return rows.map(toStoredRow);
|
||||
},
|
||||
|
||||
async getByDeviceId(
|
||||
userId: string,
|
||||
deviceId: string
|
||||
): Promise<StoredRow | undefined> {
|
||||
const row = getDatabase()
|
||||
.prepare(
|
||||
`SELECT ${ROW_COLUMNS} FROM fcm_registrations WHERE user_id = ? AND device_id = ?`
|
||||
)
|
||||
.get(userId, deviceId) as DbRow | undefined;
|
||||
return row === undefined ? undefined : toStoredRow(row);
|
||||
},
|
||||
|
||||
async getByFcmToken(fcmToken: string): Promise<StoredRow | undefined> {
|
||||
// Prefer non-legacy rows; within that pool, newest updated_at wins.
|
||||
const row = getDatabase()
|
||||
.prepare(
|
||||
`
|
||||
SELECT ${ROW_COLUMNS} FROM fcm_registrations
|
||||
WHERE fcm_token = ?
|
||||
ORDER BY (user_id = '__legacy__') ASC, updated_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
)
|
||||
.get(fcmToken) as DbRow | undefined;
|
||||
return row === undefined ? undefined : toStoredRow(row);
|
||||
},
|
||||
|
||||
async getByFcmTokenForUser(
|
||||
userId: string,
|
||||
fcmToken: string
|
||||
): Promise<StoredRow | undefined> {
|
||||
const row = getDatabase()
|
||||
.prepare(
|
||||
`
|
||||
SELECT ${ROW_COLUMNS} FROM fcm_registrations
|
||||
WHERE user_id = ? AND fcm_token = ?
|
||||
LIMIT 1
|
||||
`
|
||||
)
|
||||
.get(userId, fcmToken) as DbRow | undefined;
|
||||
return row === undefined ? undefined : toStoredRow(row);
|
||||
},
|
||||
|
||||
async update(id: string, patch: { lastNotifiedAt: number }): Promise<void> {
|
||||
getDatabase()
|
||||
.prepare(
|
||||
`UPDATE fcm_registrations SET last_notified_at = ? WHERE id = ?`
|
||||
)
|
||||
.run(patch.lastNotifiedAt, id);
|
||||
},
|
||||
};
|
||||
64
src/db/sqlite.ts
Normal file
64
src/db/sqlite.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const dataDir =
|
||||
process.env.FCM_TOKEN_DATA_DIR ?? path.join(process.cwd(), "data");
|
||||
const dbFile = path.join(dataDir, "notify.sqlite");
|
||||
|
|
||||
|
||||
const SCHEMA_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS fcm_registrations (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
fcm_token TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
test_mode INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_notified_at INTEGER
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_fcm_registrations_user_device
|
||||
ON fcm_registrations (user_id, device_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_user_id
|
||||
ON fcm_registrations (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_device_id
|
||||
ON fcm_registrations (device_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_fcm_token
|
||||
ON fcm_registrations (fcm_token);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_user_fcm_token
|
||||
ON fcm_registrations (user_id, fcm_token);
|
||||
`;
|
||||
|
||||
let database: Database.Database | null = null;
|
||||
|
||||
function ensureSchema(connection: Database.Database): void {
|
||||
connection.exec(SCHEMA_SQL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a singleton SQLite connection with schema initialized.
|
||||
* SQLite is the service's storage backend.
|
||||
*/
|
||||
export function getDatabase(): Database.Database {
|
||||
if (database === null) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
database = new Database(dbFile);
|
||||
database.pragma("journal_mode = WAL");
|
||||
ensureSchema(database);
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
/** Closes the singleton connection. Intended for tests and graceful shutdown. */
|
||||
export function closeDatabase(): void {
|
||||
if (database !== null) {
|
||||
database.close();
|
||||
database = null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import express, { Router } from "express";
|
||||
import { db } from "../db/fcmTokens.js";
|
||||
import { db } from "../db/fcmTokensSqlite.js";
|
||||
import {
|
||||
requireAuth,
|
||||
requireAuthOrNotificationLocalTest,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express, { Router } from "express";
|
||||
import { db } from "../db/fcmTokens.js";
|
||||
import { db } from "../db/fcmTokensSqlite.js";
|
||||
import {
|
||||
requireAuthOrNotificationLocalTest,
|
||||
requireEndorserAuth,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db } from "./db/fcmTokens.js";
|
||||
import { db } from "./db/fcmTokensSqlite.js";
|
||||
import { sendPushToDevice } from "./services/pushService.js";
|
||||
import { errorMessage, formatElapsedMs } from "./util/formatElapsed.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, type StoredRow } from "../db/fcmTokens.js";
|
||||
import { db, type StoredRow } from "../db/fcmTokensSqlite.js";
|
||||
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
|
||||
import { maskToken } from "../util/maskToken.js";
|
||||
import { messaging } from "./firebase.js";
|
||||
|
||||
Reference in New Issue
Block a user
Let's make this DB a generic name for all wakeup notification purposes, eg. "notify.sqlite"
I renamed the database file accordingly.
I asked AI about also changing the name to FCM_TOKEN_DATA_DIR (as a what-if), and it brought up good points for and against. I'm curious what your thoughts are on it?
I'm not clear on that... it sounds like you are saying that it would be "FCM_TOKEN_DATA_DIR.sqlite", but it seems odd to put a "_DIR" name on the file.
Maybe you mean to have another environment variable like FCM_TOKEN_DATA_FILENAME, so the full change would be something like this:
That seems reasonable, but my feeling is that it adds some complexity (eg. if people name it something else then the file is harder to find) and it doesn't seem like the benefit of flexibility is worth it.
Or maybe you meant something else.
Ahh sorry, I bungled the wording on that last reply. I meant "…changing the name of FCM_TOKEN_DATA_DIR" (the name of the environment variable that specifies the directory where notify.sqlite resides).