Initial Commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { queryGeneric } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
import { characterStateValidator } from "./schema";
|
||||
|
||||
const safeAssetSyncValidator = v.object({
|
||||
status: characterStateValidator,
|
||||
syncedAt: v.optional(v.number()),
|
||||
expiresAt: v.optional(v.number()),
|
||||
});
|
||||
|
||||
const safeCharacterValidator = v.object({
|
||||
id: v.number(),
|
||||
userId: v.string(),
|
||||
name: v.string(),
|
||||
scopes: v.array(v.string()),
|
||||
assets: safeAssetSyncValidator,
|
||||
});
|
||||
|
||||
function isUsableSession(session: any, timestamp = Date.now()): boolean {
|
||||
return Boolean(
|
||||
session &&
|
||||
!session.revokedAt &&
|
||||
session.expiresAt > timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser-facing character status. Convex validates RS256 issuer/audience
|
||||
* through auth.config.ts; the checks here make the application boundary and
|
||||
* session revocation explicit as well. No owner hashes, tokens, generations,
|
||||
* or raw synchronization errors cross this projection.
|
||||
*/
|
||||
export const characters = queryGeneric({
|
||||
args: {},
|
||||
returns: v.union(v.null(), v.array(safeCharacterValidator)),
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) return null;
|
||||
|
||||
const expectedIssuer = process.env.APP_AUTH_ISSUER;
|
||||
if (!expectedIssuer || identity.issuer !== expectedIssuer) return null;
|
||||
// Convex's customJwt provider enforces applicationID/audience before this
|
||||
// query runs. `aud` is a reserved claim and is not exposed on UserIdentity;
|
||||
// only the application issuer and session binding are checked here.
|
||||
const customIdentity = identity as typeof identity & { sid?: unknown };
|
||||
const sessionId = typeof customIdentity.sid === "string" ? customIdentity.sid : null;
|
||||
if (!sessionId) return null;
|
||||
|
||||
const session = await ctx.db
|
||||
.query("sessions")
|
||||
.withIndex("by_session_id", (q: any) => q.eq("sessionId", sessionId))
|
||||
.first();
|
||||
if (!isUsableSession(session) || session.userId !== identity.subject) return null;
|
||||
|
||||
const rows = await ctx.db
|
||||
.query("characters")
|
||||
.withIndex("by_user_id", (q: any) => q.eq("userId", identity.subject))
|
||||
.collect();
|
||||
return rows.map((character: any) => ({
|
||||
id: character.characterId,
|
||||
userId: identity.subject,
|
||||
name: character.name,
|
||||
scopes: character.scopes,
|
||||
assets: {
|
||||
status: character.state,
|
||||
...(character.syncedAt === undefined ? {} : { syncedAt: character.syncedAt }),
|
||||
...(character.expiresAt === undefined ? {} : { expiresAt: character.expiresAt }),
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user