Skip to content
17 changes: 9 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,6 @@ const AuthSchemaFields = z

const OAuthSchemaFields = z
.object({
hmacSecret: z.string().optional(),
issuer: z.string().optional(),
clientId: z.string().optional(),
})
.default({});
Expand Down Expand Up @@ -434,7 +432,6 @@ const ENV_OVERRIDES: readonly EnvOverride[] = [
{ env: "ZEROID_URL", path: "zeroidUrl", kind: "string" },
{ env: "ZEROID_ISSUER", path: "auth.issuer", kind: "string" },
{ env: "ZEROID_AUDIENCE", path: "auth.audience", kind: "string" },
{ env: "CODEOID_HMAC_SECRET", path: "oauth.hmacSecret", kind: "string" },
{ env: "CODEOID_OAUTH_CLIENT_ID", path: "oauth.clientId", kind: "string" },
{ env: "ZEROID_ACCOUNT_ID", path: "agentIdentity.accountId", kind: "string" },
{ env: "ZEROID_PROJECT_ID", path: "agentIdentity.projectId", kind: "string" },
Expand Down Expand Up @@ -559,13 +556,17 @@ export function loadConfig(opts: LoadOptions = {}): CodeoidConfig {
const memoryDbPath = configRelResolve(parsed.memory.dbPath);
const memoryCacheDir = configRelResolve(parsed.memory.modelCacheDir);

// 5. Assemble OAuth only when hmacSecret is present — keeps optionality.
const oauth: OAuthConfig | undefined = parsed.oauth.hmacSecret
// 5. Assemble OAuth when Google credentials are present in env.
const googleClientId = env.GOOGLE_CLIENT_ID;
const googleClientSecret = env.GOOGLE_CLIENT_SECRET;

const oauth: OAuthConfig | undefined =
googleClientId && googleClientSecret
? {
hmacSecret: parsed.oauth.hmacSecret,
issuer: parsed.oauth.issuer ?? resolvedZeroidUrl,
tokenEndpoint: `${resolvedZeroidUrl}/oauth2/token`,
zeroidTokenEndpoint: `${resolvedZeroidUrl}/oauth2/token`,
clientId: parsed.oauth.clientId ?? "codeoid",
googleClientId,
googleClientSecret,
accountId: parsed.agentIdentity.accountId,
projectId: parsed.agentIdentity.projectId,
allowedRedirectUris: [
Expand Down
7 changes: 7 additions & 0 deletions src/daemon/identity-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface VerifiedUser {
avatarUrl?: string;
/** Which provider verified this user */
provider: string;
/**
* Raw OIDC ID token from the upstream IdP — preserved so the daemon can
* forward it to ZeroID's token-exchange endpoint for final token issuance.
* Only set for external IdPs (Google etc.), not for LocalProvider.
*/
rawIdToken?: string;
}

export interface IdentityProvider {
Expand Down Expand Up @@ -133,6 +139,7 @@ export class GoogleOAuthProvider implements IdentityProvider {
name: payload.name,
avatarUrl: payload.picture,
provider: "google",
rawIdToken: tokens.id_token,
};
}
}
Expand Down
88 changes: 88 additions & 0 deletions src/daemon/memory/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ import { resolve, isAbsolute } from "node:path";
import type { Episode, FileReadRecord, RecallQuery } from "./types.js";
import type { TurnUsage } from "../../protocol/types.js";

export interface DailyUsageBucket {
day: string;
costUsd: number;
inputTokens: number;
outputTokens: number;
numTurns: number;
numSessions: number;
}

export interface LifetimeUsageTotals {
costUsd: number;
inputTokens: number;
outputTokens: number;
numTurns: number;
numSessions: number;
}

/** Row shape as stored in SQLite. */
interface EpisodeRow {
id: string;
Expand Down Expand Up @@ -393,6 +410,77 @@ export class SqliteEpisodeStore {
return (row?.last_turn ?? 0) + 1;
}

dailyUsage(days = 30, sessionIds?: string[]): DailyUsageBucket[] {
const sessionFilter =
sessionIds && sessionIds.length > 0
? `AND session_id IN (${sessionIds.map(() => "?").join(",")})`
: "";
const rows = this.#db
.prepare(
`SELECT
date(created_at / 1000, 'unixepoch') AS day,
COALESCE(SUM(total_cost_usd), 0) AS cost_usd,
COALESCE(SUM(input_tokens), 0) AS input_tokens,
COALESCE(SUM(output_tokens), 0) AS output_tokens,
COUNT(*) AS num_turns,
COUNT(DISTINCT session_id) AS num_sessions
FROM turn_usage
WHERE date(created_at / 1000, 'unixepoch') >= date('now', printf('-%d days', ? - 1))
${sessionFilter}
GROUP BY day
ORDER BY day ASC`,
)
.all(days, ...(sessionIds ?? [])) as Array<{
day: string;
cost_usd: number;
input_tokens: number;
output_tokens: number;
num_turns: number;
num_sessions: number;
}>;

return rows.map((r) => ({
day: r.day,
costUsd: r.cost_usd,
inputTokens: r.input_tokens,
outputTokens: r.output_tokens,
numTurns: r.num_turns,
numSessions: r.num_sessions,
}));
}

lifetimeTotals(sessionIds?: string[]): LifetimeUsageTotals {
const sessionFilter =
sessionIds && sessionIds.length > 0
? `WHERE session_id IN (${sessionIds.map(() => "?").join(",")})`
: "";
const row = this.#db
.prepare(
`SELECT
COALESCE(SUM(total_cost_usd), 0) AS cost_usd,
COALESCE(SUM(input_tokens), 0) AS input_tokens,
COALESCE(SUM(output_tokens), 0) AS output_tokens,
COUNT(*) AS num_turns,
COUNT(DISTINCT session_id) AS num_sessions
FROM turn_usage ${sessionFilter}`,
)
.get(...(sessionIds ?? [])) as {
cost_usd: number;
input_tokens: number;
output_tokens: number;
num_turns: number;
num_sessions: number;
};

return {
costUsd: row?.cost_usd ?? 0,
inputTokens: row?.input_tokens ?? 0,
outputTokens: row?.output_tokens ?? 0,
numTurns: row?.num_turns ?? 0,
numSessions: row?.num_sessions ?? 0,
};
}

#rowToTurnUsage(row: {
turn_number: number;
created_at: number;
Expand Down
Loading
Loading