From badbaed8bcb84391795ab758b259a84a6205be0d Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 11:20:46 -0800 Subject: [PATCH 01/15] feat: add prefix matching for shared ledger access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an invited user calls ensureCloudToken, check if they have access to any ledger whose name starts with the appId before creating a new one. This allows invited users to access the owner's ledger instead of creating a duplicate. 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/public/ensure-cloud-token.ts | 21 ++++- dashboard/backend/tests/db-api.test.ts | 83 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index 770bf4ae2..ee7f33bf1 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -1,7 +1,7 @@ import { Result } from "@adviser/cement"; import { DashAuthType, ReqEnsureCloudToken, ResEnsureCloudToken } from "@fireproof/core-protocols-dashboard"; import { FPCloudClaimSchema } from "@fireproof/core-types-protocols-cloud"; -import { eq, and, count } from "drizzle-orm"; +import { eq, and, count, like } from "drizzle-orm"; import { sqlAppIdBinding } from "../sql/app-id-bind.js"; import { sqlLedgers, sqlLedgerUsers } from "../sql/ledgers.js"; import { FPApiSQLCtx, FPTokenContext, ReqWithVerifiedAuthUser, VerifiedAuthUser } from "../types.js"; @@ -87,6 +87,25 @@ export async function ensureCloudToken( if (!tenantId) { return Result.Err(`no tenant found for binding of appId:${req.appId} userId:${req.auth.user.userId}`); } + if (!ledgerId) { + // Check if user has access to a ledger with name starting with appId (for shared/invited users) + const sharedLedger = await ctx.db + .select() + .from(sqlLedgerUsers) + .innerJoin(sqlLedgers, eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId)) + .where( + and( + eq(sqlLedgerUsers.userId, req.auth.user.userId), + eq(sqlLedgerUsers.status, "active"), + like(sqlLedgers.name, `${req.appId}%`), + ), + ) + .get(); + if (sharedLedger) { + ledgerId = sharedLedger.Ledgers.ledgerId; + tenantId = sharedLedger.Ledgers.tenantId; + } + } if (!ledgerId) { // create ledger const rCreateLedger = await createLedger(ctx, { diff --git a/dashboard/backend/tests/db-api.test.ts b/dashboard/backend/tests/db-api.test.ts index 7932c158e..3476ba66e 100644 --- a/dashboard/backend/tests/db-api.test.ts +++ b/dashboard/backend/tests/db-api.test.ts @@ -1146,6 +1146,89 @@ describe("db-api", () => { expect(validTenant.isErr()).toBeTruthy(); }); + /** + * Policy: Shared Ledger Access via Prefix Matching + * + * When a user is invited to a ledger and later calls ensureCloudToken with the same appId, + * they should be granted access to the existing shared ledger rather than creating a new one. + * + * The ledger name format is: `{appId}-{ownerUserId}` + * When an invited user calls ensureCloudToken with appId, the system should: + * 1. First check for an exact appId binding (existing behavior) + * 2. If not found, check if user has access to any ledger whose name starts with appId (prefix match) + * 3. Only create a new ledger if no matching ledger is found + * + * This enables collaborative apps where multiple users share the same database: + * - User A creates app with appId "vf-my-app-install1-todos" + * - System creates ledger "vf-my-app-install1-todos-{userA_id}" + * - User A invites User B to the ledger + * - User B visits app with same appId → gets access to User A's ledger (not a new one) + */ + it("ensureCloudToken grants invited user access to shared ledger via prefix match", async () => { + const userA = datas[7]; + const userB = datas[8]; + const id = sthis.nextId().str; + const appId = `SHARED_LEDGER_TEST-${id}`; + + // User A creates a ledger via ensureCloudToken + // This creates a ledger named "{appId}-{userA_id}" and binds it to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + const userALedgerId = userAToken.Ok().ledger; + + // User A invites User B to the ledger (not just the tenant) + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: userA.reqs.auth, + ticket: { + query: { + existingUserId: userB.ress.user.userId, + }, + invitedParams: { + ledger: { + id: userALedgerId, + role: "member", + right: "write", + }, + }, + }, + }); + expect(invite.isOk()).toBeTruthy(); + + // User B redeems the invite (adds them to LedgerUsers) + const redeem = await fpApi.redeemInvite({ + type: "reqRedeemInvite", + auth: userB.reqs.auth, + }); + expect(redeem.isOk()).toBeTruthy(); + + // User B calls ensureCloudToken with the same appId + // Without prefix matching, this would create a NEW ledger "{appId}-{userB_id}" + // With prefix matching, User B should get access to User A's existing ledger + const userBToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userB.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userBToken.isOk()).toBeTruthy(); + + // Critical assertion: User B should get the SAME ledger as User A + expect(userBToken.Ok().ledger).toBe(userALedgerId); + + // Verify they're using the same tenant too + expect(userBToken.Ok().tenant).toBe(userAToken.Ok().tenant); + }); + it("ensureCloudToken auto-redeems pending invite for ledger access", async () => { // User A (datas[5]) creates a ledger and binds it to an appId const userA = datas[5]; From f60369e1f93e1ddb9dd11392cba5db58f9921f0e Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 14:16:20 -0800 Subject: [PATCH 02/15] fix: remove userId suffix from ledger name and broken prefix match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger names should just be the appId for shared access. The userId suffix was causing issues with invite-based sharing. Also removed the broken LIKE query that was failing on D1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/public/ensure-cloud-token.ts | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index ee7f33bf1..b67327cd9 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -1,7 +1,7 @@ import { Result } from "@adviser/cement"; import { DashAuthType, ReqEnsureCloudToken, ResEnsureCloudToken } from "@fireproof/core-protocols-dashboard"; import { FPCloudClaimSchema } from "@fireproof/core-types-protocols-cloud"; -import { eq, and, count, like } from "drizzle-orm"; +import { eq, and, count } from "drizzle-orm"; import { sqlAppIdBinding } from "../sql/app-id-bind.js"; import { sqlLedgers, sqlLedgerUsers } from "../sql/ledgers.js"; import { FPApiSQLCtx, FPTokenContext, ReqWithVerifiedAuthUser, VerifiedAuthUser } from "../types.js"; @@ -87,25 +87,6 @@ export async function ensureCloudToken( if (!tenantId) { return Result.Err(`no tenant found for binding of appId:${req.appId} userId:${req.auth.user.userId}`); } - if (!ledgerId) { - // Check if user has access to a ledger with name starting with appId (for shared/invited users) - const sharedLedger = await ctx.db - .select() - .from(sqlLedgerUsers) - .innerJoin(sqlLedgers, eq(sqlLedgers.ledgerId, sqlLedgerUsers.ledgerId)) - .where( - and( - eq(sqlLedgerUsers.userId, req.auth.user.userId), - eq(sqlLedgerUsers.status, "active"), - like(sqlLedgers.name, `${req.appId}%`), - ), - ) - .get(); - if (sharedLedger) { - ledgerId = sharedLedger.Ledgers.ledgerId; - tenantId = sharedLedger.Ledgers.tenantId; - } - } if (!ledgerId) { // create ledger const rCreateLedger = await createLedger(ctx, { @@ -113,7 +94,7 @@ export async function ensureCloudToken( auth: req.auth, ledger: { tenantId, - name: `${req.appId}-${req.auth.user.userId}`, + name: req.appId, }, }); if (rCreateLedger.isErr()) { From ae6071679f554faa400f9e220070e9f15ae0179a Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 14:29:38 -0800 Subject: [PATCH 03/15] fix: queryCondition now matches by email OR userId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously when existingUserId was provided, it only searched by userId, ignoring email. This meant invites created by email weren't found when the invited user already existed. Now we OR together all conditions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/sql/sql-helper.ts | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dashboard/backend/sql/sql-helper.ts b/dashboard/backend/sql/sql-helper.ts index 810623d60..73d86090d 100644 --- a/dashboard/backend/sql/sql-helper.ts +++ b/dashboard/backend/sql/sql-helper.ts @@ -1,5 +1,5 @@ import { Queryable, QueryUser } from "@fireproof/core-protocols-dashboard"; -import { and, eq, or } from "drizzle-orm/sql/expressions"; +import { eq, or } from "drizzle-orm/sql/expressions"; import { SQLiteColumn } from "drizzle-orm/sqlite-core"; export function toUndef(v: string | null | undefined): string | undefined { @@ -28,15 +28,18 @@ export function queryCondition( readonly queryProvider: SQLiteColumn; }, ) { + // Build conditions for matching by userId, email, or nick + // We OR together all possible matches so invites can be found by email even when userId is provided + const conditions = [] as ReturnType[]; + if (query.existingUserId) { - return eq(table.userId, query.existingUserId); + conditions.push(eq(table.userId, query.existingUserId)); } const str = query.byString?.trim(); if (str) { const byEmail = queryEmail(str); const byNick = queryNick(str); - const conditions = [] as ReturnType[]; if (byEmail) { conditions.push(eq(table.queryEmail, byEmail)); } @@ -44,26 +47,23 @@ export function queryCondition( conditions.push(eq(table.queryNick, byNick)); } conditions.push(eq(table.userId, str)); - return or(...conditions); } const byEmail = queryEmail(query.byEmail); const byNick = queryNick(query.byNick); - let where: ReturnType = eq(table.userId, Math.random() + ""); - if (byEmail && byNick && query.andProvider) { - where = and(eq(table.queryEmail, byEmail), eq(table.queryNick, byNick), eq(table.queryProvider, query.andProvider)); - } else if (byEmail && byNick) { - where = and(eq(table.queryEmail, byEmail), eq(table.queryNick, byNick)); - } else if (byEmail && query.andProvider) { - where = and(eq(table.queryEmail, byEmail), eq(table.queryProvider, query.andProvider)); - } else if (byNick && query.andProvider) { - where = and(eq(table.queryNick, byNick), eq(table.queryProvider, query.andProvider)); - } else if (byEmail) { - where = eq(table.queryEmail, byEmail); - } else if (byNick) { - where = eq(table.queryNick, byNick); + if (byEmail) { + conditions.push(eq(table.queryEmail, byEmail)); + } + if (byNick) { + conditions.push(eq(table.queryNick, byNick)); } - return where; + + if (conditions.length === 0) { + // No valid conditions - return a condition that matches nothing + return eq(table.userId, Math.random() + ""); + } + + return or(...conditions); } export function queryNick(nick?: string): string | undefined { From ad39b1103a8bb62aaa89764ebc0f4ec19e03c01b Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 15:32:57 -0800 Subject: [PATCH 04/15] fix: handle duplicate ledger user gracefully with onConflictDoNothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user is already in a ledger, the insert now returns the existing record instead of failing with a constraint violation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/internal/add-user-to-ledger.ts | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/dashboard/backend/internal/add-user-to-ledger.ts b/dashboard/backend/internal/add-user-to-ledger.ts index 2034e6471..5b93cca2f 100644 --- a/dashboard/backend/internal/add-user-to-ledger.ts +++ b/dashboard/backend/internal/add-user-to-ledger.ts @@ -63,23 +63,46 @@ export async function addUserToLedger(ctx: FPApiSQLCtx, req: AddUserToLedger): P .where(and(eq(sqlLedgerUsers.userId, req.userId), ne(sqlLedgerUsers.default, 0))) .run(); } - const ret = ( - await ctx.db - .insert(sqlLedgerUsers) - .values({ - ledgerId: ledger.Ledgers.ledgerId, - userId: req.userId, - name: req.userName, - role: req.role, - right: req.right, - default: req.default ? 1 : 0, - createdAt: now, - updatedAt: now, - }) - .returning() - )[0]; + const inserted = await ctx.db + .insert(sqlLedgerUsers) + .values({ + ledgerId: ledger.Ledgers.ledgerId, + userId: req.userId, + name: req.userName, + role: req.role, + right: req.right, + default: req.default ? 1 : 0, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing() + .returning(); + if (inserted.length === 0) { + // User already exists in ledger, fetch existing record + const existing = await ctx.db + .select() + .from(sqlLedgerUsers) + .where(and(eq(sqlLedgerUsers.ledgerId, ledger.Ledgers.ledgerId), eq(sqlLedgerUsers.userId, req.userId))) + .get(); + if (!existing) { + return Result.Err("failed to insert or find ledger user"); + } + return Result.Ok({ + ledgerName: toUndef(ledger.Ledgers.name), + userName: toUndef(existing.name), + ledgerId: ledger.Ledgers.ledgerId, + tenantId: ledger.Ledgers.tenantId, + status: existing.status as UserStatus, + statusReason: existing.statusReason, + userId: req.userId, + default: !!existing.default, + role: toRole(existing.role), + right: toReadWrite(existing.right), + }); + } + const ret = inserted[0]; return Result.Ok({ - ledgerName: ledger.Ledgers.name, + ledgerName: toUndef(ledger.Ledgers.name), userName: req.userName, ledgerId: ledger.Ledgers.ledgerId, tenantId: ledger.Ledgers.tenantId, From 448e6cf0408ea62dc6bcd00480d5d2360bb878a0 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 15:38:20 -0800 Subject: [PATCH 05/15] fix: check for existing AppIdBinding before creating new ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an invited user visits a vibe, the AppIdBinding lookup with user join fails because they're not in LedgerUsers yet. Now we also check for existing bindings without the user join to use the existing ledger. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/public/ensure-cloud-token.ts | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index b67327cd9..2607a9689 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -88,39 +88,53 @@ export async function ensureCloudToken( return Result.Err(`no tenant found for binding of appId:${req.appId} userId:${req.auth.user.userId}`); } if (!ledgerId) { - // create ledger - const rCreateLedger = await createLedger(ctx, { - type: "reqCreateLedger", - auth: req.auth, - ledger: { - tenantId, - name: req.appId, - }, - }); - if (rCreateLedger.isErr()) { - return Result.Err(rCreateLedger.Err()); - } - ledgerId = rCreateLedger.Ok().ledger.ledgerId; - const maxBindings = await ctx.db - .select({ - total: count(sqlAppIdBinding.appId), - }) + // Check if there's already an AppIdBinding for this appId (without user join) + // This handles the case where an invited user visits before being added to the ledger + const existingBinding = await ctx.db + .select() .from(sqlAppIdBinding) - .where(eq(sqlAppIdBinding.appId, req.appId)) + .innerJoin(sqlLedgers, eq(sqlLedgers.ledgerId, sqlAppIdBinding.ledgerId)) + .where(and(eq(sqlAppIdBinding.appId, req.appId), eq(sqlAppIdBinding.env, req.env ?? "prod"))) .get(); - if (maxBindings && maxBindings.total >= ctx.params.maxAppIdBindings) { - return Result.Err(`max appId bindings reached for appId:${req.appId}`); + if (existingBinding) { + // Use the existing ledger - the user should have been added via invite redemption + ledgerId = existingBinding.Ledgers.ledgerId; + tenantId = existingBinding.Ledgers.tenantId; + } else { + // create ledger + const rCreateLedger = await createLedger(ctx, { + type: "reqCreateLedger", + auth: req.auth, + ledger: { + tenantId, + name: req.appId, + }, + }); + if (rCreateLedger.isErr()) { + return Result.Err(rCreateLedger.Err()); + } + ledgerId = rCreateLedger.Ok().ledger.ledgerId; + const maxBindings = await ctx.db + .select({ + total: count(sqlAppIdBinding.appId), + }) + .from(sqlAppIdBinding) + .where(eq(sqlAppIdBinding.appId, req.appId)) + .get(); + if (maxBindings && maxBindings.total >= ctx.params.maxAppIdBindings) { + return Result.Err(`max appId bindings reached for appId:${req.appId}`); + } + await ctx.db + .insert(sqlAppIdBinding) + .values({ + appId: req.appId, + env: req.env ?? "prod", + ledgerId, + tenantId, + createdAt: new Date().toISOString(), + }) + .run(); } - await ctx.db - .insert(sqlAppIdBinding) - .values({ - appId: req.appId, - env: req.env ?? "prod", - ledgerId, - tenantId, - createdAt: new Date().toISOString(), - }) - .run(); } } else { ledgerId = binding.Ledgers.ledgerId; From 282e47d317152068cdac6dd4ca3c66aa2a21040b Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 15:51:20 -0800 Subject: [PATCH 06/15] fix: enforce strict policy - verify user in LedgerUsers before issuing token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an existing AppIdBinding is found, we must verify the user is actually in LedgerUsers with active status before granting access. Users must accept the invite first. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/public/ensure-cloud-token.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index 2607a9689..e825c0fc2 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -97,7 +97,21 @@ export async function ensureCloudToken( .where(and(eq(sqlAppIdBinding.appId, req.appId), eq(sqlAppIdBinding.env, req.env ?? "prod"))) .get(); if (existingBinding) { - // Use the existing ledger - the user should have been added via invite redemption + // Verify the user is actually in LedgerUsers before granting access (strict policy) + const userInLedger = await ctx.db + .select() + .from(sqlLedgerUsers) + .where( + and( + eq(sqlLedgerUsers.ledgerId, existingBinding.Ledgers.ledgerId), + eq(sqlLedgerUsers.userId, req.auth.user.userId), + eq(sqlLedgerUsers.status, "active"), + ), + ) + .get(); + if (!userInLedger) { + return Result.Err(`user not authorized for ledger - please accept the invite first`); + } ledgerId = existingBinding.Ledgers.ledgerId; tenantId = existingBinding.Ledgers.tenantId; } else { From f725f5bd9bff10517eb0d476160026017383dfd0 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 15:55:05 -0800 Subject: [PATCH 07/15] test: add strict policy tests for ensureCloudToken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests verify: 1. Uninvited users are rejected when accessing existing ledgers 2. Invited users can access ledgers after redemption 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/tests/db-api.test.ts | 93 ++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/dashboard/backend/tests/db-api.test.ts b/dashboard/backend/tests/db-api.test.ts index 3476ba66e..238913e8e 100644 --- a/dashboard/backend/tests/db-api.test.ts +++ b/dashboard/backend/tests/db-api.test.ts @@ -1281,6 +1281,99 @@ describe("db-api", () => { expect(userBToken.Ok().ledger).toBe(userAToken.Ok().ledger); expect(userBToken.Ok().tenant).toBe(userAToken.Ok().tenant); }); + + it("ensureCloudToken rejects uninvited user trying to access existing ledger (strict policy)", async () => { + // User A creates a ledger via ensureCloudToken + const userA = datas[3]; + const userC = datas[4]; // User C has NOT been invited + const id = sthis.nextId().str; + const appId = `STRICT_POLICY_TEST-${id}`; + + // User A creates a ledger bound to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + + // User C tries to access the same appId WITHOUT being invited + // Under strict policy, this should fail + const userCToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userC.reqs.auth, + appId, + }, + jwkPack.opts, + ); + + // User C should be rejected - they're not in LedgerUsers for this ledger + expect(userCToken.isErr()).toBeTruthy(); + expect(userCToken.Err().message).toContain("not authorized"); + }); + + it("ensureCloudToken allows invited user after redemption", async () => { + // Same as above but User D IS invited + const userA = datas[3]; + const userD = datas[9]; + const id = sthis.nextId().str; + const appId = `INVITED_ACCESS_TEST-${id}`; + + // User A creates a ledger bound to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + const ledgerId = userAToken.Ok().ledger; + + // User A invites User D to the ledger + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: userA.reqs.auth, + ticket: { + query: { + existingUserId: userD.ress.user.userId, + }, + invitedParams: { + ledger: { + id: ledgerId, + role: "member", + right: "write", + }, + }, + }, + }); + expect(invite.isOk()).toBeTruthy(); + + // User D redeems the invite + const redeem = await fpApi.redeemInvite({ + type: "reqRedeemInvite", + auth: userD.reqs.auth, + }); + expect(redeem.isOk()).toBeTruthy(); + + // Now User D should be able to access the ledger + const userDToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userD.reqs.auth, + appId, + }, + jwkPack.opts, + ); + + expect(userDToken.isOk()).toBeTruthy(); + expect(userDToken.Ok().ledger).toBe(ledgerId); + }); }); it("create session with claim", async () => { From 48165a8ccbadee1898c08d1d2b6b42c300cc627a Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:11:35 -0800 Subject: [PATCH 08/15] fix: auto-redeem invites in ensureCloudToken via ensureUser call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/public/ensure-cloud-token.ts | 5 ++ dashboard/backend/tests/db-api.test.ts | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index e825c0fc2..a5e385f08 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -97,6 +97,11 @@ export async function ensureCloudToken( .where(and(eq(sqlAppIdBinding.appId, req.appId), eq(sqlAppIdBinding.env, req.env ?? "prod"))) .get(); if (existingBinding) { + // Auto-redeem any pending invites for this user (ensureUser calls redeemInvite) + await ensureUser(ctx, { + type: "reqEnsureUser", + auth: req.auth.verifiedAuth, + }); // Verify the user is actually in LedgerUsers before granting access (strict policy) const userInLedger = await ctx.db .select() diff --git a/dashboard/backend/tests/db-api.test.ts b/dashboard/backend/tests/db-api.test.ts index 238913e8e..48b98a385 100644 --- a/dashboard/backend/tests/db-api.test.ts +++ b/dashboard/backend/tests/db-api.test.ts @@ -1374,6 +1374,63 @@ describe("db-api", () => { expect(userDToken.isOk()).toBeTruthy(); expect(userDToken.Ok().ledger).toBe(ledgerId); }); + + it("ensureCloudToken auto-redeems invite without explicit redeemInvite call", async () => { + // This tests that ensureCloudToken itself will redeem pending invites + // Before the fix, this would fail because the user wasn't in LedgerUsers + const userA = datas[2]; + const userE = datas[1]; // User E will be invited but NOT explicitly call redeemInvite + const id = sthis.nextId().str; + const appId = `AUTO_REDEEM_TEST-${id}`; + + // User A creates a ledger bound to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + const ledgerId = userAToken.Ok().ledger; + + // User A invites User E to the ledger + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: userA.reqs.auth, + ticket: { + query: { + existingUserId: userE.ress.user.userId, + }, + invitedParams: { + ledger: { + id: ledgerId, + role: "member", + right: "write", + }, + }, + }, + }); + expect(invite.isOk()).toBeTruthy(); + + // User E does NOT explicitly call redeemInvite + // Instead, they directly call ensureCloudToken + // This should auto-redeem the invite and grant access + const userEToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userE.reqs.auth, + appId, + }, + jwkPack.opts, + ); + + // Before the fix, this would fail with "user not authorized for ledger" + // After the fix, ensureCloudToken calls ensureUser which calls redeemInvite + expect(userEToken.isOk()).toBeTruthy(); + expect(userEToken.Ok().ledger).toBe(ledgerId); + }); }); it("create session with claim", async () => { From ad0e56ebe58e4b173b0aa042251a2e44f21aa51e Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:22:46 -0800 Subject: [PATCH 09/15] fix: ensureCloudToken creates user first for new invited users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureCloudToken now calls ensureUser before checking auth, allowing brand new users invited by email to access shared ledgers on first visit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/api.ts | 8 +- dashboard/backend/tests/db-api.test.ts | 127 +++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 2fe54ae73..6b3e2f95f 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -154,7 +154,13 @@ export class FPApiSQL implements FPApiInterface { getCertFromCsr(req: ReqCertFromCsr): Promise> { return this.#checkAuth(req, (req) => getCertFromCsr(this.ctx, req)); } - ensureCloudToken(req: ReqEnsureCloudToken, ictx: Partial = {}): Promise> { + async ensureCloudToken(req: ReqEnsureCloudToken, ictx: Partial = {}): Promise> { + // For new users, ensureUser must be called first to create them + // This allows invited users to access shared ledgers on first visit + const rUser = await ensureUser(this.ctx, { type: "reqEnsureUser", auth: req.auth }); + if (rUser.isErr()) { + return Result.Err(rUser.Err()); + } return this.#checkAuth(req, (req) => ensureCloudToken(this.ctx, req, ictx)); } diff --git a/dashboard/backend/tests/db-api.test.ts b/dashboard/backend/tests/db-api.test.ts index 48b98a385..605f6dcee 100644 --- a/dashboard/backend/tests/db-api.test.ts +++ b/dashboard/backend/tests/db-api.test.ts @@ -1431,6 +1431,133 @@ describe("db-api", () => { expect(userEToken.isOk()).toBeTruthy(); expect(userEToken.Ok().ledger).toBe(ledgerId); }); + + it("ensureCloudToken auto-redeems invite by email (real UI flow)", async () => { + // This tests the actual flow: invite by email, user visits with matching email + const userA = datas[5]; + const userF = datas[6]; // User F will be invited BY EMAIL + const id = sthis.nextId().str; + const appId = `EMAIL_INVITE_TEST-${id}`; + + // User A creates a ledger bound to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + const ledgerId = userAToken.Ok().ledger; + + // Get User F's email from their auth params (simulating what invite UI does) + // The TestApiToken generates email as `test${userId}@test.de` + const userFEmail = `testuserId-${userF.reqs.auth.token}@test.de`; + + // User A invites User F BY EMAIL (like the real UI does) + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: userA.reqs.auth, + ticket: { + query: { + byEmail: userFEmail, + }, + invitedParams: { + ledger: { + id: ledgerId, + role: "member", + right: "write", + }, + }, + }, + }); + expect(invite.isOk()).toBeTruthy(); + + // User F does NOT explicitly call redeemInvite + // They just visit the vibe URL which calls ensureCloudToken + const userFToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userF.reqs.auth, + appId, + }, + jwkPack.opts, + ); + + // This should auto-redeem because ensureUser->redeemInvite finds by email + expect(userFToken.isOk()).toBeTruthy(); + expect(userFToken.Ok().ledger).toBe(ledgerId); + }); + + it("ensureCloudToken auto-redeems for brand new user invited by email", async () => { + // This tests the exact production scenario: + // - User A creates a vibe and invites by email + // - User B has NEVER visited before (no user record exists) + // - User B clicks link, logs in, and ensureCloudToken is called + const userA = datas[7]; + const id = sthis.nextId().str; + const appId = `NEW_USER_EMAIL_INVITE-${id}`; + + // User A creates a ledger bound to appId + const userAToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: userA.reqs.auth, + appId, + }, + jwkPack.opts, + ); + expect(userAToken.isOk()).toBeTruthy(); + const ledgerId = userAToken.Ok().ledger; + + // Create a completely new user token that has NEVER been used + // This simulates a user who has never visited the system + const newUserToken = `brand-new-user-${id}`; + const newUserEmail = `testuserId-${newUserToken}@test.de`; + + // User A invites the NEW user BY EMAIL + const invite = await fpApi.inviteUser({ + type: "reqInviteUser", + auth: userA.reqs.auth, + ticket: { + query: { + byEmail: newUserEmail, + }, + invitedParams: { + ledger: { + id: ledgerId, + role: "member", + right: "write", + }, + }, + }, + }); + expect(invite.isOk()).toBeTruthy(); + + // NEW user visits for the first time - they have no user record yet + // ensureCloudToken should: + // 1. Find existing binding for appId + // 2. Call ensureUser (which creates user + redeems invite) + // 3. Grant access to the ledger + const newUserAuth: DashAuthType = { + type: "clerk", + token: newUserToken, + }; + + const newUserCloudToken = await fpApi.ensureCloudToken( + { + type: "reqEnsureCloudToken", + auth: newUserAuth, + appId, + }, + jwkPack.opts, + ); + + // Should succeed because ensureUser->redeemInvite finds invite by email + expect(newUserCloudToken.isOk()).toBeTruthy(); + expect(newUserCloudToken.Ok().ledger).toBe(ledgerId); + }); }); it("create session with claim", async () => { From d2555e63b613ccb2fc81938feeead8ae9b1f4320 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:29:05 -0800 Subject: [PATCH 10/15] chore: add p0.46 version logging to ensureCloudToken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/api.ts b/dashboard/backend/api.ts index 6b3e2f95f..aeac55f9b 100644 --- a/dashboard/backend/api.ts +++ b/dashboard/backend/api.ts @@ -155,12 +155,15 @@ export class FPApiSQL implements FPApiInterface { return this.#checkAuth(req, (req) => getCertFromCsr(this.ctx, req)); } async ensureCloudToken(req: ReqEnsureCloudToken, ictx: Partial = {}): Promise> { - // For new users, ensureUser must be called first to create them + // p0.46 - For new users, ensureUser must be called first to create them // This allows invited users to access shared ledgers on first visit + console.log("[ensureCloudToken p0.46] starting for appId:", req.appId); const rUser = await ensureUser(this.ctx, { type: "reqEnsureUser", auth: req.auth }); if (rUser.isErr()) { + console.log("[ensureCloudToken p0.46] ensureUser failed:", rUser.Err()); return Result.Err(rUser.Err()); } + console.log("[ensureCloudToken p0.46] ensureUser succeeded, userId:", rUser.Ok().user.userId); return this.#checkAuth(req, (req) => ensureCloudToken(this.ctx, req, ictx)); } From 4b64c2857dbcd0c3a1435d8678d5d24652754514 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:33:52 -0800 Subject: [PATCH 11/15] chore: add p0.47 detailed logging to redeemInvite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/public/ensure-user.ts | 4 +++- dashboard/backend/public/redeem-invite.ts | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dashboard/backend/public/ensure-user.ts b/dashboard/backend/public/ensure-user.ts index f3683adb6..0b6fbf375 100644 --- a/dashboard/backend/public/ensure-user.ts +++ b/dashboard/backend/public/ensure-user.ts @@ -121,10 +121,12 @@ export async function ensureUser(ctx: FPApiSQLCtx, req: ReqEnsureUser): Promise< } } // Auto-redeem any pending invites for this user - await redeemInvite(ctx, { + console.log("[ensureUser p0.47] calling redeemInvite for email:", auth.verifiedAuth.params.email); + const redeemResult = await redeemInvite(ctx, { type: "reqRedeemInvite", auth, }); + console.log("[ensureUser p0.47] redeemInvite result:", JSON.stringify(redeemResult)); return Result.Ok({ type: "resEnsureUser", user: user, diff --git a/dashboard/backend/public/redeem-invite.ts b/dashboard/backend/public/redeem-invite.ts index 177a53416..64fa24dff 100644 --- a/dashboard/backend/public/redeem-invite.ts +++ b/dashboard/backend/public/redeem-invite.ts @@ -13,21 +13,22 @@ export async function redeemInvite( ctx: FPApiSQLCtx, req: ReqWithVerifiedAuthUser, ): Promise> { + const query = { + byString: req.auth.verifiedAuth.params.email, + byNick: req.auth.verifiedAuth.params.nick, + existingUserId: req.auth.user.userId, + }; + console.log("[redeemInvite p0.47] searching with query:", JSON.stringify(query)); + const foundInvites = await findInvite(ctx, { query }); + console.log("[redeemInvite p0.47] found invites:", foundInvites.length, "pending:", foundInvites.filter((i) => i.status === "pending").length); + if (foundInvites.length > 0) { + console.log("[redeemInvite p0.47] invite details:", JSON.stringify(foundInvites.map((i) => ({ id: i.inviteId, status: i.status, queryEmail: i.query.byEmail, ledger: i.invitedParams.ledger?.id })))); + } return Result.Ok({ type: "resRedeemInvite", invites: sqlToInviteTickets( await Promise.all( - ( - await findInvite(ctx, { - query: { - byString: req.auth.verifiedAuth.params.email, - byNick: req.auth.verifiedAuth.params.nick, - existingUserId: req.auth.user.userId, - // TODO - // andProvider: req.auth.verifiedAuth.provider, - }, - }) - ) + foundInvites .filter((i) => i.status === "pending") .map(async (invite) => { if (invite.invitedParams.tenant) { From dd3d430bf12c8c6503f8101e3448969acf872686 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:37:23 -0800 Subject: [PATCH 12/15] style: format redeem-invite.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/public/redeem-invite.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dashboard/backend/public/redeem-invite.ts b/dashboard/backend/public/redeem-invite.ts index 64fa24dff..ec045ef48 100644 --- a/dashboard/backend/public/redeem-invite.ts +++ b/dashboard/backend/public/redeem-invite.ts @@ -20,9 +20,24 @@ export async function redeemInvite( }; console.log("[redeemInvite p0.47] searching with query:", JSON.stringify(query)); const foundInvites = await findInvite(ctx, { query }); - console.log("[redeemInvite p0.47] found invites:", foundInvites.length, "pending:", foundInvites.filter((i) => i.status === "pending").length); + console.log( + "[redeemInvite p0.47] found invites:", + foundInvites.length, + "pending:", + foundInvites.filter((i) => i.status === "pending").length, + ); if (foundInvites.length > 0) { - console.log("[redeemInvite p0.47] invite details:", JSON.stringify(foundInvites.map((i) => ({ id: i.inviteId, status: i.status, queryEmail: i.query.byEmail, ledger: i.invitedParams.ledger?.id })))); + console.log( + "[redeemInvite p0.47] invite details:", + JSON.stringify( + foundInvites.map((i) => ({ + id: i.inviteId, + status: i.status, + queryEmail: i.query.byEmail, + ledger: i.invitedParams.ledger?.id, + })), + ), + ); } return Result.Ok({ type: "resRedeemInvite", From 274d08edcc234ae53f491d72eee18f30404a6e24 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:47:00 -0800 Subject: [PATCH 13/15] Add p0.48 logging to inviteUser for debugging invite creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/public/invite-user.ts | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/dashboard/backend/public/invite-user.ts b/dashboard/backend/public/invite-user.ts index 2612898ea..392f7cbcc 100644 --- a/dashboard/backend/public/invite-user.ts +++ b/dashboard/backend/public/invite-user.ts @@ -9,17 +9,30 @@ import { createInviteTicket } from "../internal/create-invite-ticket.js"; import { updateInviteTicket } from "../internal/update-invite.ticket.js"; export async function inviteUser(ctx: FPApiSQLCtx, req: ReqWithVerifiedAuthUser): Promise> { + console.log( + "[inviteUser p0.48] starting with request:", + JSON.stringify({ + query: req.ticket.query, + invitedParams: req.ticket.invitedParams, + inviteId: req.ticket.inviteId, + }), + ); if (!isVerifiedUserActive(req.auth)) { + console.log("[inviteUser p0.48] user not active"); return Result.Err(new UserNotFoundError()); } const findUser = await queryUser(ctx.db, req.ticket.query); if (findUser.isErr()) { + console.log("[inviteUser p0.48] queryUser failed:", findUser.Err()); return Result.Err(findUser.Err()); } + console.log("[inviteUser p0.48] queryUser found:", findUser.Ok().length, "users"); if (req.ticket.query.existingUserId && findUser.Ok().length !== 1) { + console.log("[inviteUser p0.48] existingUserId not found"); return Result.Err("existingUserId not found"); } if (req.ticket.query.existingUserId === req.auth.user.userId) { + console.log("[inviteUser p0.48] cannot invite self"); return Result.Err("cannot invite self"); } @@ -34,38 +47,60 @@ export async function inviteUser(ctx: FPApiSQLCtx, req: ReqWithVerifiedAuthUser< let tenantId: string | undefined; let ledgerId: string | undefined; if (req.ticket.invitedParams?.ledger) { + console.log("[inviteUser p0.48] looking for ledger:", req.ticket.invitedParams.ledger.id); const ledger = await ctx.db.select().from(sqlLedgers).where(eq(sqlLedgers.ledgerId, req.ticket.invitedParams.ledger.id)).get(); if (!ledger) { + console.log("[inviteUser p0.48] ledger not found"); return Result.Err("ledger not found"); } ledgerId = ledger.ledgerId; tenantId = ledger.tenantId; + console.log("[inviteUser p0.48] found ledger:", ledgerId, "tenantId:", tenantId); } if (req.ticket.invitedParams?.tenant) { + console.log("[inviteUser p0.48] looking for tenant:", req.ticket.invitedParams.tenant.id); const tenant = await ctx.db.select().from(sqlTenants).where(eq(sqlTenants.tenantId, req.ticket.invitedParams.tenant.id)).get(); if (!tenant) { + console.log("[inviteUser p0.48] tenant not found"); return Result.Err("tenant not found"); } tenantId = tenant.tenantId; + console.log("[inviteUser p0.48] found tenant:", tenantId); } if (!tenantId) { + console.log("[inviteUser p0.48] no tenant found"); return Result.Err("tenant not found"); } let inviteTicket: InviteTicket; if (!req.ticket.inviteId) { + console.log("[inviteUser p0.48] creating new invite for tenantId:", tenantId, "ledgerId:", ledgerId); const rInviteTicket = await createInviteTicket(ctx, req.auth.user.userId, tenantId, ledgerId, req); if (rInviteTicket.isErr()) { + console.log("[inviteUser p0.48] createInviteTicket failed:", rInviteTicket.Err()); return Result.Err(rInviteTicket.Err()); } inviteTicket = rInviteTicket.Ok(); + console.log( + "[inviteUser p0.48] created invite:", + JSON.stringify({ + inviteId: inviteTicket.inviteId, + ledgerId: inviteTicket.invitedParams.ledger?.id, + tenantId: inviteTicket.invitedParams.tenant?.id, + queryEmail: inviteTicket.query.byEmail, + }), + ); } else { + console.log("[inviteUser p0.48] updating invite:", req.ticket.inviteId); const rInviteTicket = await updateInviteTicket(ctx, req.auth.user.userId, tenantId, ledgerId, req); if (rInviteTicket.isErr()) { + console.log("[inviteUser p0.48] updateInviteTicket failed:", rInviteTicket.Err()); return Result.Err(rInviteTicket.Err()); } inviteTicket = rInviteTicket.Ok(); + console.log("[inviteUser p0.48] updated invite:", inviteTicket.inviteId); } + console.log("[inviteUser p0.48] success, returning invite:", inviteTicket.inviteId); return Result.Ok({ type: "resInviteUser", invite: inviteTicket, From 8970ac8088d7241d71da8e5d8fc8c8702cd9cf44 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:54:25 -0800 Subject: [PATCH 14/15] Add p0.49 logging to ensureCloudToken for appId binding debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- dashboard/backend/public/ensure-cloud-token.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index a5e385f08..2dd40ef8d 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -97,6 +97,7 @@ export async function ensureCloudToken( .where(and(eq(sqlAppIdBinding.appId, req.appId), eq(sqlAppIdBinding.env, req.env ?? "prod"))) .get(); if (existingBinding) { + console.log("[ensureCloudToken p0.49] existingBinding found for appId:", req.appId, "ledgerId:", existingBinding.Ledgers.ledgerId); // Auto-redeem any pending invites for this user (ensureUser calls redeemInvite) await ensureUser(ctx, { type: "reqEnsureUser", @@ -114,6 +115,7 @@ export async function ensureCloudToken( ), ) .get(); + console.log("[ensureCloudToken p0.49] userInLedger check for ledgerId:", existingBinding.Ledgers.ledgerId, "userId:", req.auth.user.userId, "found:", !!userInLedger); if (!userInLedger) { return Result.Err(`user not authorized for ledger - please accept the invite first`); } From abc0e81a1b1df50c8c39f10b92418cb7226a45e3 Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Mon, 15 Dec 2025 16:56:26 -0800 Subject: [PATCH 15/15] style: format ensure-cloud-token.ts --- dashboard/backend/public/ensure-cloud-token.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dashboard/backend/public/ensure-cloud-token.ts b/dashboard/backend/public/ensure-cloud-token.ts index 2dd40ef8d..fb312155c 100644 --- a/dashboard/backend/public/ensure-cloud-token.ts +++ b/dashboard/backend/public/ensure-cloud-token.ts @@ -97,7 +97,12 @@ export async function ensureCloudToken( .where(and(eq(sqlAppIdBinding.appId, req.appId), eq(sqlAppIdBinding.env, req.env ?? "prod"))) .get(); if (existingBinding) { - console.log("[ensureCloudToken p0.49] existingBinding found for appId:", req.appId, "ledgerId:", existingBinding.Ledgers.ledgerId); + console.log( + "[ensureCloudToken p0.49] existingBinding found for appId:", + req.appId, + "ledgerId:", + existingBinding.Ledgers.ledgerId, + ); // Auto-redeem any pending invites for this user (ensureUser calls redeemInvite) await ensureUser(ctx, { type: "reqEnsureUser", @@ -115,7 +120,14 @@ export async function ensureCloudToken( ), ) .get(); - console.log("[ensureCloudToken p0.49] userInLedger check for ledgerId:", existingBinding.Ledgers.ledgerId, "userId:", req.auth.user.userId, "found:", !!userInLedger); + console.log( + "[ensureCloudToken p0.49] userInLedger check for ledgerId:", + existingBinding.Ledgers.ledgerId, + "userId:", + req.auth.user.userId, + "found:", + !!userInLedger, + ); if (!userInLedger) { return Result.Err(`user not authorized for ledger - please accept the invite first`); }