Compare commits

...

3 Commits

Author SHA1 Message Date
2dba6c3597 Merge branch 'master' into docker 2026-06-05 19:30:50 -06:00
Jose Olarte III
6ba7d678c6 feat(notifications): allow local debug register/refresh without JWT
When the Notification Debug Panel sends testMode: true and omits
Authorization, skip requireAuth on /notifications/register and /refresh
and scope devices under a synthetic local-test user id. Requests with
a Bearer token or without testMode still use full JWT auth unchanged.
2026-06-04 20:32:18 +08:00
Jose Olarte III
dffb86007e fix(cors): enable preflight for Capacitor WebView requests
Add express cors middleware with reflected origin so Android clients
from https://localhost receive Access-Control-Allow-* headers on
OPTIONS and can proceed with POST requests.
2026-06-04 18:26:22 +08:00
6 changed files with 4036 additions and 155 deletions

3796
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@
"@peculiar/asn1-ecc": "^2.7.0", "@peculiar/asn1-ecc": "^2.7.0",
"@peculiar/asn1-schema": "^2.7.0", "@peculiar/asn1-schema": "^2.7.0",
"cbor-x": "^1.6.4", "cbor-x": "^1.6.4",
"cors": "^2.8.6",
"did-jwt": "^7.4.7", "did-jwt": "^7.4.7",
"did-resolver": "^4.1.0", "did-resolver": "^4.1.0",
"express": "^5.2.1", "express": "^5.2.1",
@@ -22,6 +23,8 @@
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^22.19.19", "@types/node": "^22.19.19",
"@types/cors": "^2.8.19",
"tsx": "^4.19.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

28
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
cbor-x: cbor-x:
specifier: ^1.6.4 specifier: ^1.6.4
version: 1.6.4 version: 1.6.4
cors:
specifier: ^2.8.6
version: 2.8.6
did-jwt: did-jwt:
specifier: ^7.4.7 specifier: ^7.4.7
version: 7.4.7 version: 7.4.7
@@ -33,6 +36,9 @@ importers:
specifier: ^4.22.3 specifier: ^4.22.3
version: 4.22.3 version: 4.22.3
devDependencies: devDependencies:
'@types/cors':
specifier: ^2.8.19
version: 2.8.19
'@types/express': '@types/express':
specifier: ^5.0.6 specifier: ^5.0.6
version: 5.0.6 version: 5.0.6
@@ -382,6 +388,9 @@ packages:
'@types/connect@3.4.38': '@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
@@ -530,6 +539,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
data-uri-to-buffer@4.0.1: data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@@ -926,6 +939,10 @@ packages:
resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==}
hasBin: true hasBin: true
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0: object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1480,6 +1497,10 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.19.19 '@types/node': 22.19.19
'@types/cors@2.8.19':
dependencies:
'@types/node': 22.19.19
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 22.19.19 '@types/node': 22.19.19
@@ -1657,6 +1678,11 @@ snapshots:
cookie@0.7.2: {} cookie@0.7.2: {}
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1: {}
debug@4.4.3: debug@4.4.3:
@@ -2193,6 +2219,8 @@ snapshots:
detect-libc: 2.1.2 detect-libc: 2.1.2
optional: true optional: true
object-assign@4.1.1: {}
object-hash@3.0.0: object-hash@3.0.0:
optional: true optional: true

View File

@@ -4,3 +4,8 @@ onlyBuiltDependencies:
- cbor-extract - cbor-extract
- esbuild - esbuild
- protobufjs - protobufjs
allowBuilds:
'@firebase/util': 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

View File

@@ -1,3 +1,4 @@
import cors from "cors";
import express from "express"; import express from "express";
import "./services/firebase.js"; import "./services/firebase.js";
import { debugRouter } from "./routes/debug.js"; import { debugRouter } from "./routes/debug.js";
@@ -7,6 +8,14 @@ import { startScheduler } from "./scheduler.js";
const app = express(); const app = express();
const port = Number(process.env.PORT) || 3000; const port = Number(process.env.PORT) || 3000;
app.use(
cors({
origin: true,
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
}),
);
app.use(express.json()); app.use(express.json());
// Keep stable for diagnostics tooling compatibility // Keep stable for diagnostics tooling compatibility

View File

@@ -1,16 +1,49 @@
import type { NextFunction, Request, Response } from "express";
import { Router } from "express"; import { Router } from "express";
import { db } from "../db/fcmTokens.js"; import { db } from "../db/fcmTokens.js";
import { requireAuth } from "../middleware/auth.js"; import { requireAuth } from "../middleware/auth.js";
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js"; import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
import { maskToken } from "../util/maskToken.js"; import { maskToken } from "../util/maskToken.js";
/** Synthetic userId for unauthenticated local debug registrations (testMode). */
const LOCAL_TEST_USER_ID = "__notification_local_test__";
function isNotificationLocalTestBypass(req: Request): boolean {
if (req.headers.authorization?.startsWith("Bearer ")) {
return false;
}
const body = req.body;
return (
body !== null &&
typeof body === "object" &&
(body as { testMode?: unknown }).testMode === true
);
}
async function requireAuthOrNotificationLocalTest(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (isNotificationLocalTestBypass(req)) {
req.did = LOCAL_TEST_USER_ID;
console.log("[Auth] Local notification test bypass");
next();
return;
}
return requireAuth(req, res, next);
}
export const notificationsRouter = Router(); export const notificationsRouter = Router();
notificationsRouter.get("/", (_req, res) => { notificationsRouter.get("/", (_req, res) => {
res.json({ ok: true, resource: "notifications" }); res.json({ ok: true, resource: "notifications" });
}); });
notificationsRouter.post("/refresh", requireAuth, async (req, res) => { notificationsRouter.post(
"/refresh",
requireAuthOrNotificationLocalTest,
async (req, res) => {
const started = Date.now(); const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
@@ -26,7 +59,9 @@ notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
const canonicalDeviceId = const canonicalDeviceId =
typeof deviceId === "string" ? deviceId.trim() : undefined; typeof deviceId === "string" ? deviceId.trim() : undefined;
const token = const token =
typeof fcmToken === "string" && fcmToken.length > 0 ? fcmToken : undefined; typeof fcmToken === "string" && fcmToken.length > 0
? fcmToken
: undefined;
console.log( console.log(
"[Refresh] Request received", "[Refresh] Request received",
@@ -76,9 +111,13 @@ notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
"deviceId=" + device.deviceId + ",", "deviceId=" + device.deviceId + ",",
"token suffix=" + maskToken(device.fcmToken) "token suffix=" + maskToken(device.fcmToken)
); );
}); }
);
notificationsRouter.post("/register", requireAuth, async (req, res) => { notificationsRouter.post(
"/register",
requireAuthOrNotificationLocalTest,
async (req, res) => {
const started = Date.now(); const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
@@ -178,4 +217,5 @@ notificationsRouter.post("/register", requireAuth, async (req, res) => {
); );
res.sendStatus(500); res.sendStatus(500);
} }
}); }
);