Repository Public
1/**2 * Auth / notification / audit DTOs of the browser API. They are NOT part of `@aos/shared`: the3 * wire shapes are byte-for-byte those of Claude Remote, so the copied auth pages keep working.4 * The web workstream imports this file by relative path (or copies it): it only depends on zod5 * and on `Branding` / `Id` from `@aos/shared`.6 */7import { z } from 'zod';8import { Branding, Id } from '@aos/shared';910const iso = z.string();1112/** The single brand of this product (`GET /auth/status` → `branding`). */13export const BRANDING: z.infer<typeof Branding> = { agent: 'aos', productName: 'Agent OS', agentName: 'Agent' };14export const PRODUCT_NAME = BRANDING.productName;1516// ---------- auth ----------17export const AuthStatus = z.object({18 setupRequired: z.boolean(), // no administrator exists yet -> setup flow with one-time token19 authenticated: z.boolean(),20 user: z.object({ id: Id, username: z.string() }).nullable(),21 methods: z.object({ password: z.boolean(), passkeys: z.number(), totp: z.boolean() }),22 csrfToken: z.string().nullable(), // required as X-CSRF-Token on every mutating request23 sessionExpiresAt: iso.nullable(),24 branding: Branding.optional(),25});26export type AuthStatus = z.infer<typeof AuthStatus>;2728export const SetupRequest = z.object({29 setupToken: z.string().min(16),30 username: z.string().min(3).max(32).regex(/^[a-z0-9_.-]+$/i),31 password: z.string().min(12).max(256),32});33export type SetupRequest = z.infer<typeof SetupRequest>;34export const LoginPasswordRequest = z.object({35 username: z.string().max(64),36 password: z.string().max(256),37 totp: z.string().regex(/^\d{6}$/).optional(),38});39export type LoginPasswordRequest = z.infer<typeof LoginPasswordRequest>;40/** Password OK but second factor still needed. */41export const LoginStep = z.object({42 ok: z.boolean(),43 next: z.enum(['done', 'totp', 'passkey', 'enroll_second_factor']),44 retryAfterSec: z.number().optional(),45});46export type LoginStep = z.infer<typeof LoginStep>;47export const TotpEnrollStart = z.object({ secret: z.string(), otpauthUrl: z.string() });48export type TotpEnrollStart = z.infer<typeof TotpEnrollStart>;49export const TotpEnrollFinish = z.object({ code: z.string().regex(/^\d{6}$/) });50export type TotpEnrollFinish = z.infer<typeof TotpEnrollFinish>;51export const Device = z.object({52 id: Id,53 kind: z.enum(['passkey', 'session']),54 label: z.string(),55 createdAt: iso,56 lastUsedAt: iso.nullable(),57 current: z.boolean(),58 userAgent: z.string().nullable(),59 ip: z.string().nullable(),60});61export type Device = z.infer<typeof Device>;6263// ---------- notifications / audit / errors ----------64/** Agent OS kinds (the Claude Remote kinds made no sense here); the rest of the shape is unchanged. */65export const NotificationKind = z.enum(['wait.approval', 'wait.question', 'mission.finished', 'system']);66export type NotificationKind = z.infer<typeof NotificationKind>;67export const Notification = z.object({68 id: Id,69 kind: NotificationKind,70 title: z.string(),71 body: z.string(), // generic by default; detail level is a user setting72 link: z.string().nullable(),73 readAt: iso.nullable(),74 createdAt: iso,75});76export type Notification = z.infer<typeof Notification>;77export const AuditEvent = z.object({78 id: z.string(),79 at: iso,80 actor: z.string(),81 action: z.string(),82 target: z.string().nullable(),83 ip: z.string().nullable(),84 ok: z.boolean(),85 detail: z.record(z.string(), z.unknown()),86});87export type AuditEvent = z.infer<typeof AuditEvent>;8889export const ApiError = z.object({90 error: z.object({ code: z.string(), message: z.string(), details: z.unknown().optional() }),91});92export type ApiError = z.infer<typeof ApiError>;9394/** WebSocket close codes used by `/api/v1/events`. */95export const WS_CLOSE = {96 UNAUTHORIZED: 4401,97 FORBIDDEN: 4403,98 BACKPRESSURE: 4429,99} as const;100