Merge pull request 'Convert JSON storage to SQLite' (#3) from sqlite-storage into master
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -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