export const NIANCODE_APP_PROTOCOL = 'niancode'; export type NianCodeDeepLink = { type: 'desktop-auth-callback'; /** Legacy desktop auth callbacks carry a request id. OIDC callbacks use state. */ requestId?: string; url: string; code?: string; state?: string; } | { type: 'cloud-agent'; slug: string; url: string } | { type: 'teacher-preview'; draftRevision: number; url: string }; let pendingTeacherPreviewRevision: number | null = null; export function queueTeacherPreviewRevision(revision: number): void { pendingTeacherPreviewRevision = revision; } export function takeTeacherPreviewRevision(): number | null { const revision = pendingTeacherPreviewRevision; pendingTeacherPreviewRevision = null; return revision; } let pendingCloudAgentRoute: string | null = null; export function queueCloudAgentLink(slug: string): void { pendingCloudAgentRoute = '/cloud-agents/shared/' + slug; } export function takeCloudAgentRoute(): string | null { const route = pendingCloudAgentRoute; pendingCloudAgentRoute = null; return route; } const SENSITIVE_QUERY_KEYS = new Set([ 'access_token', 'refresh_token', 'device_secret', 'token', ]); const AUTH_CALLBACK_QUERY_KEYS = new Set(['request_id', 'code', 'state']); export function parseNianCodeDeepLinkUrl(rawUrl: string): NianCodeDeepLink | null { let url: URL; try { url = new URL(rawUrl); } catch { return null; } if (url.protocol === `${NIANCODE_APP_PROTOCOL}:` && url.hostname === 'agents' && /^\/ml-[a-f0-9]{32}$/.test(url.pathname) && !url.username && !url.password && !url.port && !url.search && !url.hash) { return { type: 'cloud-agent', slug: url.pathname.slice(1), url: rawUrl }; } if (url.protocol !== `${NIANCODE_APP_PROTOCOL}:`) { return null; } if (url.hostname === 'coding-teacher' && url.pathname === '/preview' && !url.username && !url.password && !url.port && !url.hash && [...url.searchParams.keys()].length === 1 && url.searchParams.has('draft_revision')) { const value = url.searchParams.get('draft_revision')!; const draftRevision = Number(value); if (/^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(draftRevision)) { return { type: 'teacher-preview', draftRevision, url: rawUrl }; } return null; } if (url.hostname !== 'auth' || url.pathname !== '/callback') { return null; } for (const key of SENSITIVE_QUERY_KEYS) { if (url.searchParams.has(key)) { return null; } } if ([...url.searchParams.keys()].some((key) => !AUTH_CALLBACK_QUERY_KEYS.has(key))) { return null; } const requestId = url.searchParams.get('request_id')?.trim() || undefined; if (requestId && requestId.length > 128) return null; const code = url.searchParams.get('code')?.trim() || undefined; const state = url.searchParams.get('state')?.trim() || undefined; if (Boolean(code) !== Boolean(state) || (code && code.length > 4096) || (state && state.length > 512)) { return null; } // The platform OIDC redirect is registered as niancode://auth/callback and // therefore returns code/state without the legacy request_id parameter. if (!requestId && !(code && state)) return null; return { type: 'desktop-auth-callback', ...(requestId ? { requestId } : {}), url: rawUrl, ...(code ? { code, state } : {}), }; } export function findNianCodeDeepLinkUrl(args: readonly string[]): string | null { return args.find((arg) => parseNianCodeDeepLinkUrl(arg) !== null) ?? null; }