74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
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 }),
|
|
},
|
|
}));
|
|
},
|
|
});
|