kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: fix device auth route validation and bearer handling

Tin 505e1ff9 08a47a68

+119 -24
+3
apps/api/drizzle/0024_device_code_timestamps.sql
··· 1 + ALTER TABLE "device_code" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL; 2 + --> statement-breakpoint 3 + ALTER TABLE "device_code" ADD COLUMN "updated_at" timestamp DEFAULT now() NOT NULL;
+7
apps/api/drizzle/meta/_journal.json
··· 169 169 "when": 1775215568801, 170 170 "tag": "0023_cold_omega_red", 171 171 "breakpoints": true 172 + }, 173 + { 174 + "idx": 24, 175 + "version": "7", 176 + "when": 1775215568802, 177 + "tag": "0024_device_code_timestamps", 178 + "breakpoints": true 172 179 } 173 180 ] 174 181 }
+5
apps/api/src/database/schema.ts
··· 669 669 onDelete: "cascade", 670 670 onUpdate: "cascade", 671 671 }), 672 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 673 + updatedAt: timestamp("updated_at", { mode: "date" }) 674 + .defaultNow() 675 + .$onUpdate(() => new Date()) 676 + .notNull(), 672 677 expiresAt: timestamp("expires_at", { mode: "date" }).notNull(), 673 678 status: text("status").notNull(), 674 679 lastPolledAt: timestamp("last_polled_at", { mode: "date" }),
+64 -16
apps/api/src/index.ts
··· 317 317 318 318 // Better Auth serves GET /auth/device as JSON. Browsers that open the API URL 319 319 // directly expect a page — redirect full document navigations to the web app. 320 - api.get("/auth/device", async (c) => { 321 - const userCode = c.req.query("user_code"); 322 - const secFetchDest = c.req.header("Sec-Fetch-Dest"); 323 - const forceUiRedirect = c.req.query("ui") === "1"; 324 - // Top-level browser tab / address bar (not `fetch()` / XHR from the SPA). 325 - // Optional `ui=1` forces redirect when Sec-Fetch-* headers are missing (e.g. some clients). 326 - if (userCode && (forceUiRedirect || secFetchDest === "document")) { 327 - const clientUrl = ( 328 - process.env.KANEO_CLIENT_URL || "http://localhost:5173" 329 - ).replace(/\/$/, ""); 330 - return c.redirect( 331 - `${clientUrl}/device?user_code=${encodeURIComponent(userCode)}`, 332 - 302, 333 - ); 334 - } 335 - return auth.handler(c.req.raw); 320 + const authDeviceQuerySchema = v.object({ 321 + user_code: v.string(), 322 + ui: v.optional(v.picklist(["1"])), 336 323 }); 324 + 325 + api.get( 326 + "/auth/device", 327 + describeRoute({ 328 + operationId: "getDeviceAuthorizationPage", 329 + tags: ["Authentication"], 330 + description: 331 + "Redirect browser-based device authorization requests to the web UI", 332 + security: [], 333 + parameters: [ 334 + { 335 + name: "user_code", 336 + in: "query", 337 + required: true, 338 + schema: { 339 + type: "string", 340 + }, 341 + description: "The device authorization user code.", 342 + }, 343 + { 344 + name: "ui", 345 + in: "query", 346 + required: false, 347 + schema: { 348 + type: "string", 349 + enum: ["1"], 350 + }, 351 + description: "Force a redirect to the web UI.", 352 + }, 353 + ], 354 + responses: { 355 + 302: { 356 + description: "Redirects the browser to the web app device screen", 357 + }, 358 + 200: { 359 + description: "Device authorization payload from Better Auth", 360 + content: { 361 + "application/json": { schema: resolver(v.any()) }, 362 + }, 363 + }, 364 + }, 365 + }), 366 + validator("query", authDeviceQuerySchema), 367 + async (c) => { 368 + const { user_code: userCode, ui } = c.req.valid("query"); 369 + const secFetchDest = c.req.header("Sec-Fetch-Dest"); 370 + const forceUiRedirect = ui === "1"; 371 + // Top-level browser tab / address bar (not `fetch()` / XHR from the SPA). 372 + // Optional `ui=1` forces redirect when Sec-Fetch-* headers are missing (e.g. some clients). 373 + if (forceUiRedirect || secFetchDest === "document") { 374 + const clientUrl = ( 375 + process.env.KANEO_CLIENT_URL || "http://localhost:5173" 376 + ).replace(/\/$/, ""); 377 + return c.redirect( 378 + `${clientUrl}/device?user_code=${encodeURIComponent(userCode)}`, 379 + 302, 380 + ); 381 + } 382 + return auth.handler(c.req.raw); 383 + }, 384 + ); 337 385 338 386 api.on(["POST", "GET", "PUT", "DELETE"], "/auth/*", (c) => { 339 387 const authHeader = c.req.header("Authorization");
+17 -6
apps/api/src/utils/authenticate-api-request.ts
··· 10 10 return auth.api.getSession({ headers }); 11 11 } 12 12 13 + function parseBearerToken(authHeader: string | undefined) { 14 + if (!authHeader) { 15 + return null; 16 + } 17 + 18 + const match = authHeader.match(/^Bearer\s+(.+)$/i); 19 + if (!match) { 20 + return null; 21 + } 22 + 23 + return match[1].replace(/\s+/g, "").trim(); 24 + } 25 + 13 26 export async function authenticateApiRequest(c: Context): Promise<void> { 14 - const authHeader = c.req.header("Authorization"); 15 - if (authHeader?.startsWith("Bearer ")) { 16 - const token = authHeader.substring(7).replace(/\s+/g, "").trim(); 27 + const token = parseBearerToken(c.req.header("Authorization")); 28 + if (token) { 17 29 const apiKeyResult = await verifyApiKey(token); 18 30 if (apiKeyResult?.valid && apiKeyResult.key) { 19 31 const key = apiKeyResult.key; ··· 55 67 userId: string; 56 68 apiKeyId?: string; 57 69 }> { 58 - const authHeader = c.req.header("Authorization"); 59 - if (authHeader?.startsWith("Bearer ")) { 60 - const token = authHeader.substring(7).replace(/\s+/g, "").trim(); 70 + const token = parseBearerToken(c.req.header("Authorization")); 71 + if (token) { 61 72 const apiKeyResult = await verifyApiKey(token); 62 73 if (apiKeyResult?.valid && apiKeyResult.key) { 63 74 return {
+1 -1
apps/docs/core/installation/environment-variables.mdx
··· 50 50 Name | Description | 51 51 --- | --- | 52 52 | `AUTH_SECRET` | The secret key for the JWT token. **Must be at least 32 characters long**, use a long, random value in production. Example: use `openssl rand -base64 32` to generate a secure key in Linux/macOS. 53 - | `DEVICE_AUTH_CLIENT_IDS` | Comma-separated list of allowed device authorization client IDs. Use this to permit trusted CLI or external app identifiers such as `kaneo-cli,my-desktop-app`. | 53 + | `DEVICE_AUTH_CLIENT_IDS` | Comma-separated list of allowed device authorization client IDs. When unset, `kaneo-cli` is allowed by default, so no extra configuration is required for the CLI. Use this to permit trusted external app identifiers such as `kaneo-cli,my-desktop-app`. | 54 54 55 55 56 56 ## Optional variables
+9 -1
apps/web/src/routes/auth/verify-otp.tsx
··· 65 65 return undefined; 66 66 }, [redirect]); 67 67 68 + const signInPath = useMemo(() => { 69 + if (!redirect) { 70 + return "/auth/sign-in"; 71 + } 72 + 73 + return `/auth/sign-in?redirect=${encodeURIComponent(redirect)}`; 74 + }, [redirect]); 75 + 68 76 const onSubmit = useCallback( 69 77 async (data: VerifyOtpFormValues) => { 70 78 setIsPending(true); ··· 199 207 <Button 200 208 type="button" 201 209 variant="outline" 202 - onClick={() => history.push("/auth/sign-in")} 210 + onClick={() => history.push(signInPath)} 203 211 className="w-full" 204 212 > 205 213 <ArrowLeft className="size-4" />
+13
tests/api-integration/device-authorization.test.ts
··· 334 334 ); 335 335 336 336 expect(res.status).toBe(401); 337 + 338 + const lowercaseSchemeRes = await app.request( 339 + `/api/project?workspaceId=${encodeURIComponent(workspaceId)}`, 340 + { 341 + headers: { 342 + authorization: "bearer definitely-not-a-real-token", 343 + Cookie: cookieJar, 344 + Origin: origin, 345 + }, 346 + }, 347 + ); 348 + 349 + expect(lowercaseSchemeRes.status).toBe(401); 337 350 }); 338 351 });