export const NIANCODE_APP_PROTOCOL = 'niancode'; export type NianCodeDeepLink = { type: 'desktop-auth-callback'; requestId: string; url: 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', ]); 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; } } const requestId = url.searchParams.get('request_id')?.trim(); if (!requestId || requestId.length > 128) { return null; } return { type: 'desktop-auth-callback', requestId, url: rawUrl, }; } export function findNianCodeDeepLinkUrl(args: readonly string[]): string | null { return args.find((arg) => parseNianCodeDeepLinkUrl(arg) !== null) ?? null; }