Conversation
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.0.50-pr.481.74f9319Prefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.0.50-pr.481.74f9319"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.0.50-pr.481.74f9319"
}
}
Preview published to npm registry — try new features instantly! |
| const authData = await readJsonFile(getAuthFilePath()); | ||
| const result = AuthDataSchema.safeParse(authData); |
There was a problem hiding this comment.
Not related to PR.
I just think this is better naming
| outdent` | ||
| Deno.serve((req: Request) => | ||
| Response.json({ | ||
| authorization: req.headers.get("authorization"), | ||
| serviceAuthorization: req.headers.get("base44-service-authorization"), | ||
| }), | ||
| ); | ||
| `, |
There was a problem hiding this comment.
Not related to PR.
Adding outdent for better formatting
| async stopAll(): Promise<void> { | ||
| await Promise.all( | ||
| Array.from(this.running, ([name, { process: proc }]) => { | ||
| this.logger.log(`Stopping function: ${name}`); | ||
| const exited = new Promise<void>((r) => proc.once("exit", () => r())); | ||
| if (process.platform === "win32" && proc.pid) { | ||
| spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"]); | ||
| } else { | ||
| proc.kill(); | ||
| } | ||
| return exited; | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Not related to PR, but this fixes flakines (I hope) on windows.
What was happening:
Test in tests/cli/dev.spec.ts was failing - this one: "forwards the service token header from Authorization to local functions"
After investigation with AI we assumed following root cause:
RunLiveHandle.stop()callschild.kill("SIGINT")(CLITestkit.ts:252). Windows doesn't support POSIX signals — Node translates this to abrupt termination, so the CLI'sSIGINT→shutdown()handler in main.ts:235 never runs andfunctionManager.stopAll()is skipped.- The spawned Deno process (function-manager.ts:118) is not in a process group on Windows, so it's orphaned with open handles on
.deno/dep_analysis_cache_v2. tmp-promise'sunsafeCleanupthen tries to delete<tempDir>/.deno/...→EBUSY.
Why .deno lands in the temp dir: testkit sets HOME=tempDir (CLITestkit.ts:76), so Deno defaults DENO_DIR to <tempDir>/.deno.
Solution - to kill process "the windows way"
There was a problem hiding this comment.
Very cool investigation, Did this came up from tests / you checked this on windows?
| const inserted = applyFLS( | ||
| stripInternalFields(await collection.insertAsync(record)), | ||
| schema, | ||
| currentUser, | ||
| "read", |
There was a problem hiding this comment.
Here and in all cases below - applying FLS check for "read" because at this point I'm returning data to the user.
So I first create - it's one permission, but when I return, I actually read and it's a different permission, so I need to verify again.
I think this only makes sense of FLS (field level security), but there is no reason to do RLS for that object, since user actually created it.
I only need to make sure that user is not getting technical fields that are not related to him.
Not that important for local dev though, but just in case
kfirstri
left a comment
There was a problem hiding this comment.
Went over all files other then rls.ts, need to finish going over it
| async stopAll(): Promise<void> { | ||
| await Promise.all( | ||
| Array.from(this.running, ([name, { process: proc }]) => { | ||
| this.logger.log(`Stopping function: ${name}`); | ||
| const exited = new Promise<void>((r) => proc.once("exit", () => r())); | ||
| if (process.platform === "win32" && proc.pid) { | ||
| spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"]); | ||
| } else { | ||
| proc.kill(); | ||
| } | ||
| return exited; | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Very cool investigation, Did this came up from tests / you checked this on windows?
| let currentUser: UserDocument | undefined; | ||
| try { | ||
| const auth = req.headers.authorization || ""; | ||
| const { payload } = | ||
| jwt.decode(auth.replace("Bearer ", ""), { complete: true }) ?? {}; | ||
| currentUser = await db | ||
| .getCollection(USER_COLLECTION) | ||
| ?.findOneAsync<UserDocument>({ email: payload?.sub }); | ||
| } catch {} |
There was a problem hiding this comment.
nit: extract user fetching to another function, not really related to "withCollection"
| @@ -147,7 +223,7 @@ export async function createEntityRoutes( | |||
| router.post( | |||
| "/:entityName/bulk", | |||
There was a problem hiding this comment.
Simplification - should maybe the logic for /:entityName and /:entityName/bulk be the same at this point?
It's same logic maintained twice for 1/N items, i guess the 1 item case can use the same bulk function
| } | ||
|
|
||
| let filteredRecord = db.prepareRecord(entityName, record); | ||
| if (schema) { |
There was a problem hiding this comment.
I dont understand something - why do we check if "schema" exists before applyFLS? schema is always defined AFAIU, maybe you mean if(schema.fls) or something?
| ?.findOneAsync({ email: payload?.sub }); | ||
|
|
||
| if (!result) { | ||
| if (!currentUser) { |
There was a problem hiding this comment.
Maybe we can make this withAuth be a shared util and will be used in both routers? currently this logic is implemented inside the withCollection
| user: Record<string, unknown>, | ||
| ): boolean { | ||
| for (const [key, expected] of Object.entries(condition)) { | ||
| const userValue = key.startsWith("data.") ? user[key.slice(5)] : user[key]; |
There was a problem hiding this comment.
isn't this what resolveTemplate does?
Note
Description
This PR implements Row-Level Security (RLS) and Field-Level Security (FLS) for the local dev server's entity API. All CRUD endpoints now decode the JWT from incoming requests to identify the current user, then enforce access control rules defined in the entity schema before reading or writing records. A Windows-compatible fix for function process cleanup is also included.
Related Issue
None
Type of Change
Changes Made
packages/cli/src/cli/dev/dev-server/db/rls.ts— pure RLS/FLS engine supporting$or,$and,$nor,$in,$nin,$ne,$alloperators and{{user.*}}template variablesentities-router.tsto decode the Bearer JWT on every request, look up the current user from the users collection, and enforceschema.rls.{read,create,update,delete}and per-fieldrls.{read,write}on all CRUD endpointscreated_by/created_by_idfields from the authenticated user on createDatabase.getSchema()helper to expose entity schemas to the routerFunctionManager.stopAll()to await process exit and usetaskkillon Windows for reliable subprocess cleanupIS_TEST=true) to prevent parallel-test port collisionsdev-security*.spec.ts) covering$in,$nin,$nor,$ne, and basic RLS scenariosTesting
npm test)Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
RLS denials on read/update/delete return 404 (not 403) to avoid leaking record existence. Create and bulk-create return 403 on denial since no record ID is involved.
🤖 Generated by Claude | 2026-04-20 11:50 UTC | 74f9319