import { queryGeneric } from "convex/server"; import { v } from "convex/values"; import { characterStateValidator, authorizationStatusValidator, characterLoginRoleValidator, datasetStatusValidator } from "./schema"; import { capabilityEnabled, parseBackendCapabilityAllowlist } from "../lib/server/release-gate"; 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()), loginRole: characterLoginRoleValidator, authorizationStatus: authorizationStatusValidator, grantRevision: v.number(), credentialVersion: v.number(), authorizationEpoch: v.number(), datasets: v.array(datasetStatusValidator), assets: safeAssetSyncValidator, }); function isUsableSession(session: any, timestamp = Date.now()): boolean { return Boolean(session && !session.revokedAt && session.expiresAt > timestamp); } function deploymentCapabilityEnabled(capability: 'subscriptions'): boolean { try { return capabilityEnabled(capability, parseBackendCapabilityAllowlist(process.env.APP_CAPABILITY_ALLOWLIST)); } catch { return false; } } /** Browser-facing status: no credentials, owner evidence, generations or raw errors. */ export const characters = queryGeneric({ args: {}, returns: v.union(v.null(), v.array(safeCharacterValidator)), handler: async (ctx) => { if (!deploymentCapabilityEnabled('subscriptions')) return null; const identity = await ctx.auth.getUserIdentity(); if (!identity) return null; const expectedIssuer = process.env.APP_AUTH_ISSUER; if (!expectedIssuer || identity.issuer !== expectedIssuer) return null; 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, loginRole: character.loginRole, authorizationStatus: character.authorizationStatus, grantRevision: character.grantRevision, credentialVersion: character.credentialVersion, authorizationEpoch: character.authorizationEpoch, datasets: character.datasetStatuses, assets: { status: character.state, ...(character.syncedAt === undefined ? {} : { syncedAt: character.syncedAt }), ...(character.expiresAt === undefined ? {} : { expiresAt: character.expiresAt }), }, })); }, });