diff --git a/.gitignore b/.gitignore index ed3e193..2b67084 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,19 @@ node_modules/ dist/ .env +.env.bak +.env.local +*.bak agents/social-listening/outputs/*.html agents/social-listening/outputs/*.json agents/social-listening/outputs/*.md +# V2 per-report on-disk artefacts (large; raw Apify dumps may carry API tokens) +briefs/ +# V2 build outputs +v2/operator-app/dist/ +v2/templates/dashboard_template/dist/ +# Claude Code per-project state (memory + plans) +.claude/ *.log .DS_Store .idea/ diff --git a/DEVELOPER_BRIEF_V2.md b/DEVELOPER_BRIEF_V2.md new file mode 100644 index 0000000..beb3db5 --- /dev/null +++ b/DEVELOPER_BRIEF_V2.md @@ -0,0 +1,1274 @@ +# Social Listening Platform, Developer Brief V3 + +> Status: spec, not yet built. This document is the source of truth. +> Version: 3.0, supersedes DEVELOPER_BRIEF_V2.md +> Last updated: 2026-04-28 +> Audience: a developer (or AI coding agent) who will implement the entire pipeline. + +--- + +## Changelog vs V2 + +V3 fixes ambiguities and contradictions surfaced by a stress-test of V2. Every change is listed here so the diff is auditable. + +**Resolved contradictions:** +1. Competitors range standardised to **3 to 15** (was 5 to 10 in Stage 0, 3 to 15 in §5). +2. Pass 1 stops scheduling at **its own cap (50% of budget)**, not 70% (was conflicting between §9 and Stage 2). +3. Frontend stack is **React 18 + Vite + TypeScript + Tailwind + Recharts** for 10a; vanilla HTML + inline JSON for 10b. §7 table updated to match. +4. Cover strategy: 10a downloads and caches locally to `dashboard/public/covers/`; 10b base64-inlines covers into the HTML so the file is fully portable. §8 rewritten. +5. Date window honours `brief.date_window_days` with default 30. Stage 2 reads from brief. +6. `lens_tags` standardised to `hooks | visual | audio | sentiment | narrative`. "behaviour" removed. +7. `dataset_v2.json` defined explicitly in §10. +8. MoM compare: if `prior_report_id` is set in brief but the report is not on disk, build **fails loudly**, not silently skips. Acceptance criteria and §16 now agree. +9. Section ordering fixed: §15 (Memory) and §16 (MoM compare) re-ordered to read in numeric sequence. + +**New section added:** +10. **§4.5 Judgement calls and rubrics.** This is the missing brain. It tells Claude (and the developer) how to make the taste calls the pipeline keeps deferring: which seeds count as good, how to pick selection filters per business question, what makes a category editorial, how to calibrate `business_question_relevance`, and how to name a trend. Without this section the same brief produces a different report every run. + +**Tightened specifications:** +11. ≥50 trends softened to **target 50, hard floor 35**. Splitting low-quality trends to hit a number is forbidden. +12. Retry policy unified across stages: **3 attempts, exponential backoff (1s, 4s, 16s)**, then fail open with explicit user choice. +13. Comments rule loosened: **30 if available, minimum 5 to validate**. Videos with fewer than 5 comments are dropped from selection with a logged reason. +14. Stage 7 batching de-dup: each batch receives the running list of `atomic_id` titles so Claude can extend, not duplicate. +15. `paid_or_organic` evidence rules clarified: only signals the pipeline can actually compute are accepted (caption tags, brand handle mentions, on-screen disclosure). The "30-day branded posts" check is dropped unless a creator-profile fetch is added. +16. Claude CLI determinism: **temperature 0**, retry-on-invalid-JSON 2x, then fail with the offending video id surfaced. +17. `kpis`, `positioning`, `notes` from the brief schema are now wired into prompts and the Methodology view, or removed. +18. Stage 9b no longer duplicates the ≥5-videos check (already enforced in Stage 8). Stage 9b only checks coverage. + +--- + +## 0. TL;DR + +Build a TikTok-first social-listening pipeline that produces an interactive trend dashboard for brand strategists. + +The pipeline runs in **two passes**: +1. **Pass 1 (broad scan):** scrape metadata + cover only for as many videos as the user's Apify budget allows. Filter to last 30 days (configurable). Rank. +2. **Pass 2 (deep analysis):** on a subset the user selects via preset filters guided by the §4.5 selection playbook, pull **Apify transcripts + up to 30 top comments + frame-by-frame visual analysis**. + +A **hard manifest gate** stops execution if any selected video is missing any required asset. No analysis runs on incomplete data. + +After deep analysis, the system produces: +- **Atomic insights** (intermediate scaffolding, hundreds). +- **Target 50 editorial trends per report (hard floor 35)**, organised into **brief-driven custom categories** (Claude generates categories per report from the brief, against the §4.5d rubric). +- An **interactive static dashboard** (React + Vite, deployable to Netlify), the primary user-facing deliverable. Reference: the Atrium dashboard at `https://splendorous-chebakia-97afdd.netlify.app/`. +- A **standalone HTML bundle for claude.ai upload** (secondary output, for ad-hoc design iteration). +- **Cross-report month-over-month comparison** view in the dashboard (new vs returning trends, velocity deltas). + +Trends are filtered by the brief's `business_question` using the §4.5c calibration rubric. Trends scoring below 0.35 are dropped; 0.35 to 0.6 are tagged `peripheral`; ≥0.6 are tagged `core` and lead the report. + +QA gates before publish: automated paid-vs-organic flagging, automated 100% data coverage check, plus human Community Manager + Brand Strategist sign-off (separate humans). + +--- + +## 1. Vision and non-negotiable principles + +### What this product is +An automated researcher that turns 30 days of TikTok activity into a 35-to-50 trend report a brand strategist can present. + +### What it is not +- Not a sentiment dashboard. We do not output "% positive". +- Not a metrics tracker. We do not chart follower counts. +- Not a content recommender. We do not tell the brand what to post. + +### Non-negotiable principles +1. **Evidence before assertion.** Every trend cites at least 5 source videos with id, handle, plays, stl, transcript snippet, and 1+ comment quotes. +2. **Paid vs organic transparency.** Every creator is auto-flagged paid, organic, or unclear, using only signals the pipeline can compute (see §4.5e). Influencer content is never described as organic without verification. (See Memory: `feedback_paid_vs_organic.md`.) +3. **English-only outputs.** All comments, transcripts, captions translated to English before insight extraction. No foreign-language strings in the final report. +4. **Manifest-gated.** Analysis cannot start until every selected video has all required assets confirmed: metadata, transcript, ≥5 comments (target 30), frame-by-frame analysis. +5. **Brief-driven.** Categories, lanes, lenses are generated from the brief Claude collects from the user, not hardcoded. +6. **Rubric-bound.** Every taste call (seed quality, selection, category quality, trend relevance, editorial naming) is governed by §4.5. No model freelancing on judgement. +7. **Quality over count.** Target 50 trends. Hard floor 35. **Never split a weak trend to hit a number.** +8. **QA mandatory.** Community Manager + Brand Strategist review checklists must pass before publish. Separate humans. (See Memory: `feedback_qa_review_mandatory.md`.) +9. **Slide-like report design.** Wide layout, flash cards, large fonts, no text walls. (See Memory: `feedback_report_design.md`.) + +--- + +## 2. Glossary + +| Term | Definition | +|---|---| +| **Brief** | The structured scope of a single report: brand, competitors, audience, geo, language, business question, KPIs. Captured conversationally, stored as `brief.yaml`. | +| **Pass 1 (broad)** | Metadata + cover scrape across hashtags, competitor handles, and brief-derived seeds. Apify-budget bounded. Last `date_window_days` (default 30). | +| **Pass 2 (deep)** | Per-video transcript, up to 30 comments, frame-by-frame visual analysis on user-selected videos. | +| **Manifest** | `manifest.json`, single source of truth listing every required asset per video and its status. Hard gate before analysis. | +| **Atomic insight** | A single observation grounded in 1 to 10 videos. Intermediate scaffolding only. Not user-facing. | +| **Trend** | Editorial unit shipped in the report. Grouped from atomic insights. Has a name, narrative, evidence, and category. **Target 50 per report; hard floor 35.** | +| **Category** | Brief-driven grouping of trends (e.g. for Dove: "Hair Rituals", "Self-Image Drama"). Generated per-report by Claude against the §4.5d rubric. | +| **Lens** | Cross-cutting view of all trends (Hooks Library, Visual Vernacular, Audio Atlas, Sentiment Map). Same lenses every report. | +| **Coverage** | % of selected videos that have all required assets. Must be 100% before analysis runs. | +| **Recipe** | A named selection-filter combination tied to a business-question archetype. See §4.5b. | + +--- + +## 3. End-to-end pipeline + +``` + ┌────────────────────────────────────────────┐ + │ Stage 0: Brief intake (conversational) │ + │ Output: brief.yaml │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 1: Seed expansion (rubric §4.5a) │ + │ Brief → hashtags + handles + search terms │ + │ Output: seeds.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 2: Pass 1 broad scrape (Apify) │ + │ Last `date_window_days`, $-budget bounded │ + │ Output: pass1_videos.json (metadata+cover)│ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 3: User selection (recipes §4.5b) │ + │ Recipe-led preset filters; user confirms │ + │ Output: selected_video_ids.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 4: Pass 2 deep enrichment │ + │ Apify transcripts + ≤30 comments per video│ + │ Frame extraction (1fps, video-length aware)│ + │ Output: enriched/{video_id}/... │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 5: Manifest validation (HARD GATE) │ + │ Every selected id must have all assets │ + │ Output: manifest.json, must be 100% green │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 6: Per-video Claude analysis │ + │ Inputs bundled per video, single prompt │ + │ Output: analysis/{video_id}.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 7: Atomic insight extraction │ + │ Hundreds of grounded observations │ + │ Output: atomic_insights.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 8: Trend synthesis (§4.5c, d, e) │ + │ Brief-driven categories, lens enrichment │ + │ Output: trends.json + categories.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 9: QA gates │ + │ Auto: paid/organic + coverage │ + │ Human: CM + Strategist checklists │ + │ Output: qa_report.json │ + └──────────────────┬─────────────────────────┘ + │ + ┌──────────────────▼─────────────────────────┐ + │ Stage 10: Output assembly │ + │ React/Vite dashboard + claude.ai HTML │ + │ Output: dashboard/dist/, dashboard.html │ + └────────────────────────────────────────────┘ +``` + +Each stage is independently runnable, idempotent, and resumable from disk caches. Stages 6, 7, 8 use Claude CLI (`claude --model claude-opus-4-6 --print --temperature 0`) to stay on Max plan, no API spend. + +--- + +## 4. Stage specifications + +### Stage 0, Brief intake (conversational) + +**Goal:** Capture a complete brief by chatting with the user. + +**Behaviour:** +- Run as a CLI command: `python3 pipeline.py brief`. +- A Python loop calling Claude CLI conversationally; one question at a time. +- Required fields: + - `brand` (object): name, handle, category, optional positioning. + - `competitors` (array, **3 to 15**): each with name and handle. + - `audience` (object): primary, optional secondary, age_range, gender, interests (3+ items). + - `geo` (string), country or region. + - `language` (string), primary language for source content (default: `en`). + - `business_question` (string), the specific question this report must answer. Treated as a first-class input throughout the pipeline. See §4.5b for archetypes. + - `kpis` (array), what success looks like for the brand (rendered in the Methodology view, prepended to the Stage 8 trend-synthesis prompt as success-context). + - `budget_usd` (number), Apify $ cap; typical range 50 to 100. +- Agent extracts → confirms back to user → writes to `briefs/{report_id}/brief.yaml`. +- Validates schema (pydantic) before writing. If a field is ambiguous, asks again. +- If `business_question` is shorter than 8 words or contains no noun phrase about the brand, the agent asks the user to refine it. Vague business questions = vague reports. + +**Output schema (`brief.yaml`):** see §5 for the full canonical schema. + +--- + +### Stage 1, Seed expansion + +**Goal:** Turn the brief into a concrete scrape input list using the §4.5a quality rubric. + +**Behaviour:** +- Read `brief.yaml`. +- Use Claude CLI (prompt: `prompts/seed_expansion.md`) to expand against the §4.5a rubric: + - `hashtags`: 30 to 50 tags, structured into three tiers (anchor, discovery, edge). Claude must label each. + - `handles`: brand + competitors + up to 10 creator handles **only if confidence is high** (mentioned in mainstream coverage or prior reports). Otherwise leave empty; Pass 1 will surface organic creators via hashtag scrapes. + - `search_terms`: 10 to 20 plain-language queries written in audience voice (e.g. "everything shower routine"), not marketing copy. +- Output stored under `briefs/{report_id}/seeds.json`. +- **User reviews and edits before Stage 2 runs.** This is a checkpoint, not optional. Display the seeds in tiers; ask for thumbs up or edit. + +**Output schema (`seeds.json`):** +```json +{ + "hashtags": { + "anchor": [{"tag": "#hairtok", "rationale": "huge volume, on-topic"}], + "discovery": [{"tag": "#scalpcare", "rationale": "tutorial-heavy niche"}], + "edge": [{"tag": "#showerritual", "rationale": "small but live"}] + }, + "handles": [{"handle": "dove", "type": "brand"}, {"handle": "olay", "type": "competitor"}], + "search_terms": [{"term": "everything shower routine", "rationale": "audience phrase"}] +} +``` + +--- + +### Stage 2, Pass 1 broad scrape (Apify, $-budget bounded) + +**Goal:** Pull as much last-`date_window_days` metadata as the budget allows. + +**Behaviour:** +- Read `seeds.json`, `brief.budget_usd`, and `brief.date_window_days` (default 30). +- Estimate per-actor cost from `config/apify_costs.json`. +- Schedule scrapes in priority order: anchor hashtags → handles → discovery hashtags → search_terms → edge hashtags. +- For each scrape: + - **Hard date filter:** last `date_window_days` only, applied at fetch time and re-validated locally. + - Actors: TikTok hashtag scraper, TikTok profile scraper, TikTok search scraper. + - Pull only what we need: id, handle, caption, hashtags, plays, likes, saves, comments_count, shares, stl, duration_sec, date, url, cover. + - **Do not pull mp4** at this stage. +- Stop scheduling new scrapes when projected cumulative cost reaches **`budget_usd × 0.5`** (the Pass 1 cap from §9). +- Deduplicate by video id. Drop videos older than `date_window_days`. +- Minimum healthy yield: 500 deduplicated videos. If Pass 1 returns fewer than 500, log a warning and prompt user to either (a) expand seeds, (b) widen the date window, or (c) proceed knowing the trend pool will be thin. + +**Output:** +- `pass1/raw/{actor}/{run_id}.json`, raw Apify dump per run. +- `pass1/pass1_videos.json`, deduplicated, normalised array. +- `pass1/spend_log.json`, actual cost per run. + +**Schema (`pass1_videos.json`):** +```json +[ + { + "id": "7280…", + "handle": "dove", + "caption": "…", + "hashtags": ["#hairtok","#showertok"], + "plays": 1200000, + "likes": 89000, + "saves": 12000, + "comments_count": 2400, + "shares": 5000, + "stl_pct": 8.4, + "duration_sec": 38, + "date": "2026-04-12", + "url": "https://www.tiktok.com/@dove/video/7280…", + "cover": "https://…", + "_source": "hashtag:hairtok", + "_scraped_at": "2026-04-28T09:12:33Z" + } +] +``` + +--- + +### Stage 3, User selection (recipe-led preset filters) + +**Goal:** Let the user pick which videos go deep, with a recipe to guide the choice. + +**Behaviour:** +- CLI command: `python3 pipeline.py select --report dove-2026-04`. +- Prints a summary of `pass1_videos.json` (total count, date range, top creators, top hashtags, plays distribution). +- **Prompts user with a recommended recipe** based on `brief.business_question` (see §4.5b for the playbook). User can accept the recipe or override with custom filters. + +**Available filter primitives:** + +| Filter | Parameter | Example | +|---|---|---| +| `top_by_plays` | N | top 100 by plays | +| `top_by_stl` | N | top 50 by stl% (videos with ≥10k plays only, to avoid small-base outliers) | +| `top_by_comments` | N | top 50 by comments_count | +| `top_by_saves` | N | top 30 by saves | +| `top_by_velocity` | N | top 50 by plays per day-since-post (videos posted ≥2 days ago only) | +| `manual_ids` | list | explicit ids the user wants in | + +**Combination grammar:** filters are combined with AND or OR; precedence is left-to-right with explicit parentheses required for nesting. Example: `(top_by_plays:100 AND top_by_stl:50) OR top_by_saves:30`. The parser will reject ambiguous combinations. + +**Sample sizing guidance (printed at prompt):** 100 to 200 selected videos is the sweet spot. Below 80 starves trend synthesis. Above 300 explodes Pass 2 cost without proportional insight. + +**Confirmation:** +- After deduplication across filters, print final count and projected Pass 2 cost: `selected_count × (transcript_unit + comments_unit + frame_unit_avg + mp4_unit)`. +- User confirms before proceeding. + +**Output:** +- `pass2/selected_video_ids.json`, array of ids. +- `pass2/selection_rules.json`, record of recipe used and filters applied (audit trail). + +--- + +### Stage 4, Pass 2 deep enrichment + +**Goal:** Bundle the required assets per selected video. + +**Per-video assets:** +1. **Transcript:** Apify TikTok subtitles actor only. No Whisper fallback. +2. **Comments:** Apify TikTok comments scraper, sorted by likes desc, **target 30, minimum 5**. Save text, author, likes, replies_count, posted_at. Videos with fewer than 5 comments are flagged and dropped from selection (logged in `pass2/dropped_videos.json`). +3. **Frame-by-frame analysis input:** extract frames from the mp4. Cap based on video length: + - ≤15s: 1 frame per second (max 15) + - 16 to 60s: 1 frame per 2 seconds (max 30) + - 61 to 180s: 1 frame per 4 seconds (max 45) + - >180s: 1 frame per 6 seconds (cap 60, hard ceiling) + - Frames stored as jpg, downscaled to 720px wide. +4. **Metadata:** already in pass1 record, copied into the per-video bundle for self-containment. +5. **Cover image:** downloaded and stored locally as `enriched/{id}/cover.jpg` (TikTok cover URLs expire; see §8). + +**Behaviour:** +- For each selected id, in parallel batches (default concurrency 4, configurable): + - Fetch transcript via Apify. + - Fetch comments via Apify. + - Download mp4 via the Apify-provided url (or the `tiktok-video-download` actor as fallback if the direct url has expired). + - Run ffmpeg locally to extract frames at the configured fps. + - Translate transcript + comments to English if `language_detected != "en"` (use Claude CLI for translation, batched 20 comments per call). Language detection via Apify subtitles metadata first, `langdetect` library as fallback. +- **Retry policy:** 3 attempts, exponential backoff 1s, 4s, 16s. After third failure, mark the asset failed and let Stage 5 surface it. +- Persist each asset under `enriched/{video_id}/`: + +``` +enriched/ + 7280…/ + metadata.json + cover.jpg + transcript.json # {language_detected, text_original, text_en} + comments.json # array of up to 30 + frames/ + 0001.jpg + 0002.jpg + … + bundle.json # join file pointing to all of the above +``` + +- `bundle.json` is the canonical per-video object the analysis stage reads. See §6. + +--- + +### Stage 5, Manifest validation (HARD GATE) + +**Goal:** Refuse to proceed unless every selected video has all required assets. + +**Behaviour:** +- CLI command: `python3 pipeline.py validate --report dove-2026-04`. +- Build `manifest.json`: + +```json +{ + "report_id": "dove-2026-04", + "selected_count": 200, + "summary": { + "metadata_ok": 200, + "transcript_ok": 198, + "comments_ok": 196, + "frames_ok": 200, + "cover_ok": 200, + "all_ok": 195, + "coverage_pct": 97.5 + }, + "videos": [ + { + "id": "7280…", + "metadata": {"ok": true, "path": "enriched/7280…/metadata.json"}, + "transcript": {"ok": true, "path": "…", "language_detected": "en"}, + "comments": {"ok": true, "count": 28, "path": "…"}, + "frames": {"ok": true, "count": 22, "path": "…"}, + "cover": {"ok": true, "path": "…"}, + "all_ok": true + }, + { + "id": "7281…", + "transcript": {"ok": false, "error": "no subtitles available"}, + "all_ok": false + } + ] +} +``` + +- **Validity rules per asset:** + - `metadata.ok`: full record present. + - `transcript.ok`: non-empty text_en. + - `comments.ok`: ≥5 comments, all with text_en. + - `frames.ok`: ≥3 frames extracted. + - `cover.ok`: local cover.jpg present and >5 KB. +- If `coverage_pct < 100`, retry the missing assets with the same exponential backoff policy (1s, 4s, 16s). +- If still incomplete, **block the next stage** and print a clear error listing every failing video and its missing asset. +- User options: + - Fix manually (re-run specific failures), then re-run validation. + - Drop failing ids (`--drop-failing` flag). The system **automatically backfills** from the next-best videos in the original recipe ranking, until target count is restored or candidates exhausted. The backfill list is logged to `pass2/backfill_log.json`. +- Analysis cannot start until manifest reports `all_ok: true` for 100% of remaining selected ids. + +--- + +### Stage 6, Per-video Claude analysis + +**Goal:** Turn each bundle into a structured analysis with a single, deterministic prompt. + +**Behaviour:** +- Claude CLI flags: `--model claude-opus-4-6 --print --temperature 0`. JSON-mode output. +- For each video where `all_ok: true`: + - Build a single prompt (template: `prompts/per_video_analysis.md`) including: + - Caption + hashtags + - English transcript + - Up to 30 English comment texts (numbered, with like-counts) + - All extracted frames (passed as inline base64 or vision-tool refs depending on Claude CLI capabilities at build time) + - Single shot. +- **Invalid JSON handling:** on parse failure, retry up to 2 times with the same prompt. If still invalid, fail with the offending video id surfaced; do not silently skip. + +**Output schema (`analysis/{video_id}.json`):** + +```json +{ + "id": "7280…", + "what_happens": "string, 2 sentence plain description", + "hook": { + "first_3_seconds": "verbatim transcript snippet", + "pattern": "shock|question|reveal|relatable|tutorial-promise|other", + "why_it_stops_scroll": "string" + }, + "visual_aesthetic": { + "lighting": "natural|harsh|soft|neon|warm|cool|mixed", + "colour_palette": ["#hex","#hex","..."], + "setting": "bathroom|bedroom|outdoor|studio|kitchen|other", + "talent": "single-creator|duo|group|none", + "products_visible": ["product names…"], + "on_screen_text_examples": ["…","…"] + }, + "format": "tutorial|confession|hot-take|review|routine|transformation|hack|skit|asmr", + "audio": { + "music_present": true, + "music_mood": "upbeat|melancholic|dreamy|aggressive|none", + "voiceover": true, + "asmr_elements": false + }, + "narrative": { + "thesis": "string, what is this video really saying", + "tension": "string, what is the conflict or interest", + "resolution": "string, how the video lands" + }, + "audience_signals": { + "comment_themes": ["self-care anxiety","wanting to copy","disbelief","aspiration"], + "comment_sentiment_split": {"positive": 22, "neutral": 5, "critical": 3}, + "verbatim_quotes": [ + {"text":"…","likes":1200,"theme":"aspiration"}, + {"text":"…","likes":900,"theme":"copycat-intent"} + ] + }, + "paid_or_organic": { + "label": "paid|organic|unclear", + "reasoning": "string, what evidence supports the label", + "evidence_signals_used": ["caption_ad_tag","caption_brand_handle","on_screen_disclosure","creator_repeat_in_report"] + }, + "_meta": { + "generated_at": "...", + "model": "claude-opus-4-6", + "input_frames": 22 + } +} +``` + +**Paid/organic evidence rules (computable signals only):** +- `caption_ad_tag`: caption contains `#ad`, `#sponsored`, `#gifted`, `#paidpartnership` (regex). +- `caption_brand_handle`: caption mentions brand or competitor handle in `@`-form. +- `on_screen_disclosure`: Claude detects "Paid partnership", "AD", or platform's TikTok disclosure label in any extracted frame. +- `creator_repeat_in_report`: same handle appears in ≥3 selected videos with brand mentions in this report. + +If none of those signals fire, label is `unclear`. Do not infer paid status from "the video looks polished." + +- Cache aggressively. If `analysis/{id}.json` exists, skip. + +--- + +### Stage 7, Atomic insight extraction + +**Goal:** Convert per-video analyses into hundreds of small, evidence-grounded observations. + +**Behaviour:** +- Iterate per-video analyses in batches of 20. +- Each batch receives: + - The 20 analyses. + - The running list of `{atomic_id, type, observation}` triples extracted so far. + - Instruction: "Extend the list. Do not duplicate. If a new observation strengthens an existing one, add the video ids to that atomic_id instead of creating a new one." +- For each batch, ask Claude to extract atomic insights of 4 types: + - **Hook insight:** a recurring opening pattern. + - **Visual insight:** a recurring aesthetic element. + - **Audio insight:** a recurring sound, music style, or voice device. + - **Narrative insight:** a recurring thesis, tension, or worldview. +- Each atomic insight cites the video ids that support it. +- `frequency` field is computed from `len(supporting_video_ids)` post-hoc, not asked of the model. Single source of truth. + +**Schema (`atomic_insights.json`):** +```json +[ + { + "atomic_id": "ATM-0001", + "type": "hook", + "observation": "Creators open with a whispered confession about over-washing", + "supporting_video_ids": ["7280…","7281…","7290…"] + } +] +``` + +- Target: 200 to 500 atomic insights. Prompt instructs Claude to err on the side of more, smaller insights rather than fewer, broader ones. + +--- + +### Stage 8, Trend synthesis (target 50, hard floor 35) + +**Goal:** Synthesise atomic insights into editorial trends, organised in brief-driven categories, governed by §4.5. + +**Step 8a, Generate categories from the brief:** +- Single Claude call. Inputs: `brief.yaml` (including business_question, audience, kpis), atomic_insights summary, and the §4.5d category rubric. +- Output: 5 to 10 categories tailored to the brief. +- Each category is rejected and re-asked if it fails the rubric (descriptive, redundant, or mechanically derived). +- Stored at `categories.json`. + +**Step 8b, Cluster atomic insights into trends:** +- For each category, surface trends. Each trend must: + - Have an editorial name conforming to the §4.5e naming rubric. + - Have a 2 to 3 sentence narrative. + - Cite at least 5 supporting video ids. + - Have a `category` field referencing one of the brief-driven categories. + - Have a `lens_tags` array, subset of `hooks | visual | audio | sentiment | narrative`. + - Have KPIs: total plays, video count, unique creators, avg stl, comment-engagement index, paid/organic split. + - Have `velocity`: recency7v30 numeric, label `accelerating | steady | declining`. + - Have `top_hooks` (5 verbatim transcript snippets). + - Have `top_comments` (5 verbatim quotes). + - Have `top_videos` (8 with id, handle, plays, stl, cover). + - Have `linked_trends` (top 5 sibling trends by Jaccard similarity on shared videos / shared hashtags / format match). + - Have `business_question_relevance`: score 0 to 1 plus a 1-sentence justification, computed in a dedicated Claude pass over the trend's narrative + the brief, **calibrated against the §4.5c anchors**. + +**Step 8b.5, Business-question filter:** +- Drop trends with `business_question_relevance < 0.35`. +- Tag remaining trends: + - 0.35 to 0.6: `peripheral`, shown lower in dashboard. + - ≥0.6: `core`, lead the report. +- **If after filtering fewer than 35 trends remain**, the orchestrator re-runs 8b with two specific instructions: + 1. "Surface additional trends from atomic insights you didn't yet cluster, especially those touching: {business_question}." + 2. "Do not split existing trends. Do not lower evidence thresholds." +- Maximum 2 re-run attempts. If still under 35, the build proceeds with a logged warning to the strategist: the trend pool is genuinely thin. Manufacturing trends to hit a count is forbidden. +- **Target 50** is a soft target. Hitting 42 with high-quality trends beats hitting 50 with padded ones. + +**Step 8c, Lens enrichment:** +- Across all trends, build cross-cutting lens artefacts: + - **Hooks Library:** 30 to 60 hook patterns, each with verbatim opener, linked trends, share %. + - **Visual Vernacular:** 8 to 12 recurring visual patterns, each with name, signal, example videos. + - **Audio Atlas:** 10 to 25 top sounds, music styles, voice devices, with frequency and example videos. + - **Sentiment Map:** 5 to 8 dominant emotional themes from comments. + +**Output (`trends.json`):** +```json +[ + { + "trend_id": "TR-001", + "name": "The Ceremonial Hair Wash", + "category": "Hair Rituals", + "narrative": "Hair-washing has become a slow ritual…", + "lens_tags": ["visual","narrative","sentiment"], + "kpis": { + "plays_total": 48000000, + "videos": 36, + "unique_creators": 28, + "avg_stl_pct": 7.4, + "comment_engagement_index": 0.062, + "paid_organic_split": {"paid": 4, "organic": 26, "unclear": 6} + }, + "velocity": {"recency7v30": 0.42, "label": "accelerating"}, + "business_question_relevance": {"score": 0.85, "tier": "core", "justification": "directly explains the cultural moment around hair washing"}, + "top_hooks": ["…"], + "top_comments": ["…"], + "top_videos": [{"id":"…","handle":"…","cover":"…","plays":1200000,"stl_pct":7.8}], + "linked_trends": [{"trend_id":"TR-014","relation":"shared aesthetic"}], + "supporting_atomic_ids": ["ATM-0001","ATM-0014","..."], + "supporting_video_ids": ["7280…","..."] + } +] +``` + +--- + +## 4.5. Judgement calls and rubrics (the brain) + +This section governs every taste call the pipeline keeps deferring. Without it the same brief produces a different report every run. With it, runs are reproducible and the model has anchors instead of vibes. + +### 4.5a. Seed quality rubric (used in Stage 1) + +**Hashtags, three tiers:** + +- **Anchor (5 to 8 tags):** huge volume (millions of views in 30d), unmistakably on-topic, native to the audience's content language. *Dove example: #hairtok, #showertok, #haircare.* +- **Discovery (15 to 20 tags):** medium volume, niche-specific, where rituals and behaviours live. *Dove example: #scalpcare, #curlyhair, #everythingshower, #hairporosity.* +- **Edge (5 to 10 tags):** small but live (posts in the last 14 days), capturing emergent vocabulary. *Dove example: #hairhealing, #scalpritual.* + +**Reject hashtags that are:** too broad (#beauty), brand-locked self-references (#dove), dead (no posts in last 14 days), or unrelated trends (#mealprep). Claude must label rejected candidates with the rejection reason in `seeds.json` so the user can override. + +**Search terms:** +- **Good:** how a real person describes the behaviour. *"everything shower routine", "scalp massage at night", "hair washing too much".* +- **Bad:** marketing copy ("luxurious haircare experience"), too narrow ("Dove shampoo review"), too generic ("hair tips"). + +**Creator handles:** +- Only include handles Claude is **highly confident exist** (mentioned in mainstream coverage, brand reports, user's own prior conversations, or returned in seed-research web fetches). +- Otherwise leave empty. Pass 1 will surface organic creators via hashtag scrapes anyway. Inventing handles wastes Apify budget on null returns. + +### 4.5b. Selection playbook (used in Stage 3) + +The recipe is recommended automatically based on the shape of `brief.business_question`. The user can accept or override. + +**Recipe A, "What stops the scroll" (hooks-focused):** +- Trigger phrases in business_question: "hook", "stops the scroll", "first three seconds", "attention". +- Filter: `top_by_stl:80 OR top_by_velocity:40`. +- Rationale: stl% is the clearest hook-quality proxy; velocity catches what's catching on right now. + +**Recipe B, "Why is X having a moment" (cultural moment, default):** +- Trigger phrases: "cultural moment", "why is", "emerging", "shift", "trend". +- Filter: `top_by_saves:60 AND (top_by_plays:100 OR top_by_comments:50)`. +- Rationale: saves signal personal resonance; plays and comments together capture mass + conversation. + +**Recipe C, "How does X position vs competitors" (competitive landscape):** +- Trigger phrases: "competitor", "positioning", "market share", "vs". +- Filter: `manual_ids:(brand+competitor handles latest 50 each) + top_by_plays:80`. +- Rationale: forces the brand and competitor sets in, then adds the cultural top to compare against. + +**Recipe D, "What do users actually feel about X" (audience truth):** +- Trigger phrases: "what do users", "audience feeling", "reception", "reaction", "sentiment". +- Filter: `top_by_comments:60 AND top_by_stl:40`. +- Rationale: comments carry the truth; stl% filters out videos no one watched long enough to react to. + +**Default if no archetype matches:** Recipe B. + +**Sample size guidance:** +- Sweet spot: **100 to 200 selected videos**. +- <80: trend synthesis starves; expect fewer than 35 trends and the build will warn. +- >300: Pass 2 cost balloons without proportional insight gain. + +### 4.5c. Trend relevance calibration (used in Stage 8b.5) + +The `business_question_relevance` score must be calibrated, not free-floating. Every trend-scoring Claude call receives these anchors in its prompt. + +**Calibration anchors:** + +| Score band | Tier | Definition | Example for Dove brief on "Why is hair washing emerging as a cultural moment?" | +|---|---|---|---| +| ≥0.80 | core | Trend directly answers the business question or names the territory the brand should claim. | "The Ceremonial Hair Wash": directly explains the cultural moment. **0.85** | +| 0.60 to 0.79 | core | Trend supports the answer materially; lead-supporting trend. | "Scalp as Self": adjacent ritual, reinforces the territory. **0.70** | +| 0.35 to 0.59 | peripheral | Trend gives context, useful but not the headline. | "Hair Texture Confidence": same audience, supports framing, not the answer. **0.45** | +| <0.35 | dropped | Real, well-evidenced trend that does not advance the business question. | "Skincare Minimalism": same audience, different category. **0.20** | + +The model is shown all four anchors in every relevance call so scores stay comparable across reports. + +### 4.5d. Category quality rubric (used in Stage 8a) + +**Good categories are:** +- Editorial and evocative (could be a magazine section name). +- Mutually exclusive (no significant overlap with another). +- 2 to 5 words. +- Cultural, not descriptive. + +*Good Dove examples:* "Hair Rituals", "Self-Image Drama", "Anti-Beauty Backlash", "Grooming as Identity". + +**Bad categories to reject:** +- Descriptive containers: "Hair Care Videos", "Beauty Content". +- Mechanically derived from data: "#hairtok content", "Top Plays". +- Redundant with another: "Hair Routines" if "Hair Rituals" already exists. +- Genre labels: "Tutorials", "Reviews". + +If any candidate fails the rubric, Claude is asked to re-generate that one category specifically, with the failure reason cited. + +### 4.5e. Editorial naming rubric (used in Stage 8b) + +**Good trend names are:** +- Phrased like a magazine headline or cultural call-out. +- Specific enough to be recognisable, abstract enough to hold many videos. + +*Good examples:* "The Ceremonial Hair Wash", "The 5-Minute Reset", "Anti-Influencer Beauty", "The Confession Routine". + +**Bad trend names to reject:** +- Hashtag literals: "#hairtok trend". +- Generic descriptors: "Hair videos", "Self-care content". +- Feature lists: "Videos with shower scenes and ASMR". +- Brand-supplied marketing language: "The Dove Difference". + +The naming pass is run as a final QA on `trends.json` before Stage 9: any name failing the rubric is sent back for one re-roll, then accepted with a flag so the human strategist sees it in QA. + +--- + +### Stage 9, QA gates + +**Goal:** Block publish until both automated and human gates pass. + +**9a, Automated: paid-vs-organic flag.** +- Every video already has `paid_or_organic.label` and `evidence_signals_used`. +- For every creator (handle), aggregate their videos in this report. If any video is `paid`, the creator is flagged paid in this report. +- Generate `qa/paid_organic_review.json`: + ```json + [ + { + "handle": "dermangelo", + "videos_in_report": 4, + "label": "paid", + "evidence_signals": ["caption_ad_tag on 2 videos","caption_brand_handle on 1"], + "needs_human_confirm": true + } + ] + ``` +- A reviewer (Brand Strategist) confirms each row before publish. + +**9b, Automated: data-coverage check.** +- Re-read `manifest.json`. If `coverage_pct < 100`, hard block. +- (Trend evidence ≥5 already enforced in Stage 8; not duplicated here.) + +**9c, Human: Community Manager checklist.** +- Surface a checklist UI in the dashboard (or printed CLI): + - [ ] All comments shown in the report are in English. + - [ ] No culturally insensitive language. + - [ ] No personal info or PII in any quoted comment. + - [ ] No factual claims that aren't sourced to a video id. + - [ ] No screenshots or covers from creators on the report's denylist (`config/creator_denylist.json`, defaults to empty; CM can add per-report exclusions). +- CM signs off in `qa/cm_signoff.json` with `signed_by`, `signed_at`. + +**9d, Human: Brand Strategist checklist.** +- Surface a checklist: + - [ ] Each trend has a clear narrative (not a hashtag). + - [ ] Each trend has ≥5 supporting videos with evidence. + - [ ] Trend names pass the §4.5e editorial rubric. + - [ ] Categories pass the §4.5d rubric. + - [ ] The trend pool spans the brief-driven categories meaningfully. + - [ ] The report answers the `business_question` from the brief. +- Strategist signs off in `qa/strategist_signoff.json`. + +**Separation of humans:** CM and Brand Strategist sign-offs are by **different humans**. The pipeline checks `cm_signoff.signed_by != strategist_signoff.signed_by` and refuses to proceed if equal. + +Stage 10 cannot run unless all four gates pass. + +--- + +### Stage 10, Output assembly + +**Goal:** Produce a deployable interactive dashboard (primary) plus a claude.ai upload bundle (secondary). + +Both outputs are built from a single intermediate file: **`outputs/dataset_v2.json`**, the assembled join of `brief.yaml`, `trends.json`, `categories.json`, all four lens artefacts, `qa/*` sign-offs, and (if present) the four `compare/*.json` files. + +**`dataset_v2.json` schema (top level):** +```json +{ + "brief": {"...": "from brief.yaml"}, + "categories": [{"...": "from categories.json"}], + "trends": [{"...": "from trends.json"}], + "lenses": { + "hooks_library": [...], + "visual_vernacular": [...], + "audio_atlas": [...], + "sentiment_map": [...] + }, + "qa": { + "paid_organic_review": [...], + "cm_signoff": {...}, + "strategist_signoff": {...} + }, + "compare": { + "new_trends": [...], + "returning_trends": [...], + "faded_trends": [...], + "category_momentum": [...] + }, + "methodology": { + "scrape_stats": {...}, + "manifest_summary": {...}, + "kpis": ["..."], + "positioning": "..." + } +} +``` + +**10a, Interactive static dashboard (React + Vite, Netlify-deployable).** + +Reference build: `https://splendorous-chebakia-97afdd.netlify.app/` (Atrium). Match its shape and feel. + +- Stack: React 18 + Vite + TypeScript + Tailwind CSS + Recharts (or Visx). No backend. +- Single repo under `briefs/{report_id}/dashboard/` scaffolded from `templates/dashboard_template/` (kept in repo). +- Reads `dataset_v2.json` at build time, bundles inline. No fetch at runtime. +- Output: `dist/` static files. Deployable via `netlify deploy --prod --dir=dist`. +- Covers cached locally to `dashboard/public/covers/{video_id}.jpg` (copied from `enriched/{id}/cover.jpg` at build). + +**Required views:** +1. **Overview:** opening line, business question, audience snapshot, headline KPIs, top-line "monday actions". +2. **Categories grid:** one card per brief-driven category with trend count, plays total, dominant format, sample covers. +3. **Trends explorer:** filterable list of all trends. Filters: category, velocity, paid/organic split, lens tags, relevance tier (core/peripheral). Sort by plays, stl, recency7v30, business-question relevance. +4. **Trend detail:** click a trend to expand: narrative, KPIs, hook quotes, comment quotes, top 8 videos with covers + links, linked trends, supporting evidence drill-down (atomic insights → individual videos). +5. **Lens views (4 tabs):** + - **Hooks Library:** 30 to 60 hook patterns, copyable openers, share %. + - **Visual Vernacular:** 8 to 12 visual patterns with example covers. + - **Audio Atlas:** top sounds, music styles, voice devices. + - **Sentiment Map:** emotional themes from comments. +6. **Charts:** + - Bubble: engagement vs reach (x = plays, y = stl%, size = video count, colour = category). + - Category distribution (treemap or bar). + - Velocity timeline (sparkline per trend, recency7v30). + - Paid vs organic stacked bar per category. +7. **Compare (Month-over-Month):** see §16. Pulls from `compare/*` artefacts. +8. **Paid creators appendix:** list flagged in QA, with evidence and report-level prevalence. +9. **Methodology:** the brief (including `kpis` and `positioning`), scrape stats, manifest coverage, QA sign-offs (read-only). + +**Design principles:** +- Slide-like density: wide layout, flash cards, large fonts, no text walls (per `feedback_report_design.md`). +- Dark + light themes; default to light. +- All copy English-only. +- Original-language strings shown only in tooltips on quoted comments, on hover, with the English translation as the primary surface. + +**10b, Standalone HTML bundle for claude.ai upload.** +- Single self-contained `dashboard.html` (≤3 MB). +- Embeds full `dataset_v2.json` inline as ` + + diff --git a/v2/operator-app/package.json b/v2/operator-app/package.json new file mode 100644 index 0000000..e8abf6b --- /dev/null +++ b/v2/operator-app/package.json @@ -0,0 +1,29 @@ +{ + "name": "operator-app", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0", + "@tanstack/react-query": "^5.51.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "vite": "^5.4.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.4.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0" + } +} diff --git a/v2/operator-app/postcss.config.js b/v2/operator-app/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/v2/operator-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/v2/operator-app/public/msal-browser.min.js b/v2/operator-app/public/msal-browser.min.js new file mode 100644 index 0000000..a72a9f3 --- /dev/null +++ b/v2/operator-app/public/msal-browser.min.js @@ -0,0 +1,2 @@ +/*! @azure/msal-browser v5.6.3 2026-04-01 */ +"use strict";!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).msal={})}(this,(function(e){const t="https://login.microsoftonline.com/common/",r="common",n=`${t}discovery/instance?api-version=1.1&authorization_endpoint=`,o=".ciamlogin.com",i="openid",s="profile",a="offline_access",c="S256",h="Not Available",l="http://169.254.169.254/metadata/instance/compute/location",d=["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"],u="GET",g="POST",p=[i,s,a],m=[...p,"email"],f="Content-Type",y="Content-Length",w="Retry-After",I="X-AnchorMailbox",C="WWW-Authenticate",v="Authentication-Info",k="x-ms-request-id",T="x-ms-httpver",b="active-account-filters",A="common",S="organizations",_="consumers",E="access_token",P="xms_cc",R={LOGIN:"login",SELECT_ACCOUNT:"select_account",CONSENT:"consent",NONE:"none",CREATE:"create",NO_SESSION:"no_session"},O="code",M="id_token token refresh_token",x={QUERY:"query",FRAGMENT:"fragment",FORM_POST:"form_post"},q="authorization_code",N="refresh_token",U="Generic",L={ID_TOKEN:"IdToken",ACCESS_TOKEN:"AccessToken",ACCESS_TOKEN_WITH_AUTH_SCHEME:"AccessToken_With_AuthScheme",REFRESH_TOKEN:"RefreshToken"},H="appmetadata",D="1",F="authority-metadata",K="config",B="cache",z="network",j="hardcoded_values",$=5,J="server-telemetry",W=",",G={BEARER:"Bearer",POP:"pop",SSH:"ssh-cert"},Q="throttling",V="1",X="3",Y="4",Z="2",ee="4",te="5",re="0",ne="1",oe="2",ie="3",se="4",ae={Jwt:"JWT",Jwk:"JWK",Pop:"pop"},ce="client_id",he="redirect_uri",le="token_type",de="req_cnf",ue="return_spa_code",ge="x-client-xtra-sku",pe="brk_client_id",me="brk_redirect_uri",fe="instance_aware";function ye(e){return`See https://aka.ms/msal.js.errors#${e} for details`}class we extends Error{constructor(e,t,r){const n=t||(e?ye(e):"");super(n?`${e}: ${n}`:e),Object.setPrototypeOf(this,we.prototype),this.errorCode=e||"",this.errorMessage=n||"",this.subError=r||"",this.name="AuthError"}setCorrelationId(e){this.correlationId=e}}function Ie(e,t){return new we(e,t||ye(e))}class Ce extends we{constructor(e){super(e),this.name="ClientConfigurationError",Object.setPrototypeOf(this,Ce.prototype)}}function ve(e){return new Ce(e)}class ke{static isEmptyObj(e){if(e)try{const t=JSON.parse(e);return 0===Object.keys(t).length}catch(e){}return!0}static startsWith(e,t){return 0===e.indexOf(t)}static endsWith(e,t){return e.length>=t.length&&e.lastIndexOf(t)===e.length-t.length}static queryStringToObject(e){const t={},r=e.split("&"),n=e=>decodeURIComponent(e.replace(/\+/g," "));return r.forEach((e=>{if(e.trim()){const[r,o]=e.split(/=(.+)/g,2);r&&o&&(t[n(r)]=n(o))}})),t}static trimArrayEntries(e){return e.map((e=>e.trim()))}static removeEmptyStringsFromArray(e){return e.filter((e=>!!e))}static jsonParseHelper(e){try{return JSON.parse(e)}catch(e){return null}}}class Te extends we{constructor(e,t){super(e,t),this.name="ClientAuthError",Object.setPrototypeOf(this,Te.prototype)}}function be(e,t){return new Te(e,t)}const Ae="redirect_uri_empty",Se="authority_uri_insecure",_e="url_parse_error",Ee="empty_url_error",Pe="empty_input_scopes_error",Re="invalid_claims",Oe="token_request_empty",Me="logout_request_empty",xe="pkce_params_missing",qe="invalid_cloud_discovery_metadata",Ne="invalid_authority_metadata",Ue="untrusted_authority",Le="missing_ssh_jwk",He="missing_ssh_kid",De="missing_nonce_authentication_header",Fe="invalid_authentication_header",Ke="cannot_set_OIDCOptions",Be="cannot_allow_platform_broker",ze="authority_mismatch",je="invalid_request_method_for_EAR";var $e=Object.freeze({__proto__:null,authorityMismatch:ze,authorityUriInsecure:Se,cannotAllowPlatformBroker:Be,cannotSetOIDCOptions:Ke,claimsRequestParsingError:"claims_request_parsing_error",emptyInputScopesError:Pe,invalidAuthenticationHeader:Fe,invalidAuthorityMetadata:Ne,invalidClaims:Re,invalidCloudDiscoveryMetadata:qe,invalidCodeChallengeMethod:"invalid_code_challenge_method",invalidRequestMethodForEAR:je,logoutRequestEmpty:Me,missingNonceAuthenticationHeader:De,missingSshJwk:Le,missingSshKid:He,pkceParamsMissing:xe,redirectUriEmpty:Ae,tokenRequestEmpty:Oe,untrustedAuthority:Ue,urlEmptyError:Ee,urlParseError:_e});const Je="client_info_decoding_error",We="client_info_empty_error",Ge="token_parsing_error",Qe="null_or_empty_token",Ve="endpoints_resolution_error",Xe="network_error",Ye="openid_config_error",Ze="hash_not_deserialized",et="invalid_state",tt="state_mismatch",rt="state_not_found",nt="nonce_mismatch",ot="auth_time_not_found",it="max_age_transpired",st="multiple_matching_appMetadata",at="request_cannot_be_made",ct="cannot_remove_empty_scope",ht="cannot_append_scopeset",lt="empty_input_scopeset",dt="no_account_in_silent_request",ut="invalid_cache_record",gt="invalid_cache_environment",pt="no_account_found",mt="no_crypto_object",ft="token_refresh_required",yt="token_claims_cnf_required_for_signedjwt",wt="authorization_code_missing_from_server_response",It="binding_key_not_removed",Ct="end_session_endpoint_not_supported",vt="key_id_missing",kt="no_network_connectivity",Tt="user_canceled",bt="method_not_implemented",At="nested_app_auth_bridge_disabled",St="resource_parameter_required",_t="misplaced_resource_parameter";var Et=Object.freeze({__proto__:null,authTimeNotFound:ot,authorizationCodeMissingFromServerResponse:wt,bindingKeyNotRemoved:It,cannotAppendScopeSet:ht,cannotRemoveEmptyScope:ct,clientInfoDecodingError:Je,clientInfoEmptyError:We,emptyInputScopeSet:lt,endSessionEndpointNotSupported:Ct,endpointResolutionError:Ve,hashNotDeserialized:Ze,invalidCacheEnvironment:gt,invalidCacheRecord:ut,invalidState:et,keyIdMissing:vt,maxAgeTranspired:it,methodNotImplemented:bt,misplacedResourceParam:_t,multipleMatchingAppMetadata:st,multipleMatchingTokens:"multiple_matching_tokens",nestedAppAuthBridgeDisabled:At,networkError:Xe,noAccountFound:pt,noAccountInSilentRequest:dt,noCryptoObject:mt,noNetworkConnectivity:kt,nonceMismatch:nt,nullOrEmptyToken:Qe,openIdConfigError:Ye,platformBrokerError:"platform_broker_error",requestCannotBeMade:at,resourceParameterRequired:St,stateMismatch:tt,stateNotFound:rt,tokenClaimsCnfRequiredForSignedJwt:yt,tokenParsingError:Ge,tokenRefreshRequired:ft,unexpectedCredentialType:"unexpected_credential_type",userCanceled:Tt});class Pt{constructor(e){const t=e?ke.trimArrayEntries([...e]):[],r=t?ke.removeEmptyStringsFromArray(t):[];if(!r||!r.length)throw ve(Pe);this.scopes=new Set,r.forEach((e=>this.scopes.add(e)))}static fromString(e){const t=(e||"").split(" ");return new Pt(t)}static createSearchScopes(e){const t=e&&e.length>0?e:[...p],r=new Pt(t);return r.containsOnlyOIDCScopes()?r.removeScope(a):r.removeOIDCScopes(),r}containsScope(e){const t=this.printScopesLowerCase().split(" "),r=new Pt(t);return!!e&&r.scopes.has(e.toLowerCase())}containsScopeSet(e){return!(!e||e.scopes.size<=0)&&(this.scopes.size>=e.scopes.size&&e.asArray().every((e=>this.containsScope(e))))}containsOnlyOIDCScopes(){let e=0;return m.forEach((t=>{this.containsScope(t)&&(e+=1)})),this.scopes.size===e}appendScope(e){e&&this.scopes.add(e.trim())}appendScopes(e){try{e.forEach((e=>this.appendScope(e)))}catch(e){throw be(ht)}}removeScope(e){if(!e)throw be(ct);this.scopes.delete(e.trim())}removeOIDCScopes(){m.forEach((e=>{this.scopes.delete(e)}))}unionScopeSets(e){if(!e)throw be(lt);const t=new Set;return e.scopes.forEach((e=>t.add(e.toLowerCase()))),this.scopes.forEach((e=>t.add(e.toLowerCase()))),t}intersectingScopeSets(e){if(!e)throw be(lt);e.containsOnlyOIDCScopes()||e.removeOIDCScopes();const t=this.unionScopeSets(e),r=e.getScopeCount(),n=this.getScopeCount();return t.sizee.push(t))),e}printScopes(){if(this.scopes){return this.asArray().join(" ")}return""}printScopesLowerCase(){return this.printScopes().toLowerCase()}}function Rt(e,t,r){if(!t)return;const n=e.get(ce);n&&e.has(pe)&&r?.addFields({embeddedClientId:n,embeddedRedirectUri:e.get(he)},t)}function Ot(e,t){e.set("response_type",t)}function Mt(e,t,r=!0,n=p){!r||n.includes("openid")||t.includes("openid")||n.push("openid");const o=r?[...t||[],...n]:t||[],i=new Pt(o);e.set("scope",i.printScopes())}function xt(e,t){e.set(ce,t)}function qt(e,t){e.set(he,t)}function Nt(e,t){e.set("login_hint",t)}function Ut(e,t){e.set(I,`UPN:${t}`)}function Lt(e,t){e.set(I,`Oid:${t.uid}@${t.utid}`)}function Ht(e,t){e.set("sid",t)}function Dt(e,t,r){const n=Yt(t,r);try{JSON.parse(n)}catch(e){throw ve(Re)}e.set("claims",n)}function Ft(e,t){e.set("client-request-id",t)}function Kt(e,t){e.set("x-client-SKU",t.sku),e.set("x-client-VER",t.version),t.os&&e.set("x-client-OS",t.os),t.cpu&&e.set("x-client-CPU",t.cpu)}function Bt(e,t){t?.appName&&e.set("x-app-name",t.appName),t?.appVersion&&e.set("x-app-ver",t.appVersion)}function zt(e,t){t&&e.set("state",t)}function jt(e,t,r){if(!t||!r)throw ve(xe);e.set("code_challenge",t),e.set("code_challenge_method",r)}function $t(e,t){e.set("client_secret",t)}function Jt(e,t){t&&e.set("client_assertion",t)}function Wt(e,t){t&&e.set("client_assertion_type",t)}function Gt(e,t){e.set("grant_type",t)}function Qt(e){e.set("client_info","1")}function Vt(e){e.has(fe)||e.set(fe,"true")}function Xt(e,t){Object.entries(t).forEach((([t,r])=>{!e.has(t)&&r&&e.set(t,r)}))}function Yt(e,t){let r;if(e)try{r=JSON.parse(e)}catch(e){throw ve(Re)}else r={};return t&&t.length>0&&(r.hasOwnProperty(E)||(r[E]={}),r[E][P]={values:t}),JSON.stringify(r)}function Zt(e,t){t&&(e.set(le,G.POP),e.set(de,t))}function er(e,t){t&&(e.set(le,G.SSH),e.set(de,t))}function tr(e,t){e.set("x-client-current-telemetry",t.generateCurrentRequestHeaderValue()),e.set("x-client-last-telemetry",t.generateLastRequestHeaderValue())}function rr(e){e.set("x-ms-lib-capability","retry-after, h429")}function nr(e,t,r){e.has(pe)||e.set(pe,t),e.has(me)||e.set(me,r)}function or(e,t){t&&e.set("resource",t)}function ir(e){if(!e)return e;let t=e.toLowerCase();return ke.endsWith(t,"?")?t=t.slice(0,-1):ke.endsWith(t,"?/")&&(t=t.slice(0,-2)),ke.endsWith(t,"/")||(t+="/"),t}function sr(e){return e.startsWith("#/")?e.substring(2):e.startsWith("#")||e.startsWith("?")?e.substring(1):e}function ar(e){if(!e||e.indexOf("=")<0)return null;try{const t=sr(e),r=Object.fromEntries(new URLSearchParams(t));if(r.code||r.ear_jwe||r.error||r.error_description||r.state)return r}catch(e){throw be(Ze)}return null}function cr(e){const t=new Array;return e.forEach(((e,r)=>{t.push(`${r}=${encodeURIComponent(e)}`)})),t.join("&")}function hr(e){if(!e)return e;const t=e.split("#")[0];try{const e=new URL(t);return ir(e.origin+e.pathname+e.search)}catch(e){return ir(t)}}const lr={createNewGuid:()=>{throw be(bt)},base64Decode:()=>{throw be(bt)},base64Encode:()=>{throw be(bt)},base64UrlEncode:()=>{throw be(bt)},encodeKid:()=>{throw be(bt)},async getPublicKeyThumbprint(){throw be(bt)},async removeTokenBindingKey(){throw be(bt)},async clearKeystore(){throw be(bt)},async signJwt(){throw be(bt)},async hashString(){throw be(bt)}};var dr;e.LogLevel=void 0,(dr=e.LogLevel||(e.LogLevel={}))[dr.Error=0]="Error",dr[dr.Warning=1]="Warning",dr[dr.Info=2]="Info",dr[dr.Verbose=3]="Verbose",dr[dr.Trace=4]="Trace";const ur=new Map;function gr(e,t){const r=Date.now();let n=ur.get(e);if(n)!function(e,t){ur.delete(e),ur.set(e,t)}(e,n);else if(n={logs:[],firstEventTime:r},ur.set(e,n),ur.size>50){const e=ur.keys().next().value;e&&ur.delete(e)}n.logs.push({...t,milliseconds:r-n.firstEventTime}),n.logs.length>500&&n.logs.shift()}class pr{constructor(t,r,n){this.level=e.LogLevel.Info;const o=t||pr.createDefaultLoggerOptions();this.localCallback=o.loggerCallback||(()=>{}),this.piiLoggingEnabled=o.piiLoggingEnabled||!1,this.level="number"==typeof o.logLevel?o.logLevel:e.LogLevel.Info,this.packageName=r||"",this.packageVersion=n||""}static createDefaultLoggerOptions(){return{loggerCallback:()=>{},piiLoggingEnabled:!1,logLevel:e.LogLevel.Info}}clone(e,t){return new pr({loggerCallback:this.localCallback,piiLoggingEnabled:this.piiLoggingEnabled,logLevel:this.level},e,t)}logMessage(t,r){const n=r.correlationId;if(function(e){if(6!==e.length)return!1;for(let t=0;t="a"&&r<="z"||r>="A"&&r<="Z"||r>="0"&&r<="9"))return!1}return!0}(t)){gr(n,{hash:t,level:r.logLevel,containsPii:r.containsPii||!1,milliseconds:0})}if(r.logLevel>this.level||!this.piiLoggingEnabled&&r.containsPii)return;const o=`${`[${(new Date).toUTCString()}] : [${n}]`} : ${this.packageName}@${this.packageVersion} : ${e.LogLevel[r.logLevel]} - ${t}`;this.executeCallback(r.logLevel,o,r.containsPii||!1)}executeCallback(e,t,r){this.localCallback&&this.localCallback(e,t,r)}error(t,r){this.logMessage(t,{logLevel:e.LogLevel.Error,containsPii:!1,correlationId:r})}errorPii(t,r){this.logMessage(t,{logLevel:e.LogLevel.Error,containsPii:!0,correlationId:r})}warning(t,r){this.logMessage(t,{logLevel:e.LogLevel.Warning,containsPii:!1,correlationId:r})}warningPii(t,r){this.logMessage(t,{logLevel:e.LogLevel.Warning,containsPii:!0,correlationId:r})}info(t,r){this.logMessage(t,{logLevel:e.LogLevel.Info,containsPii:!1,correlationId:r})}infoPii(t,r){this.logMessage(t,{logLevel:e.LogLevel.Info,containsPii:!0,correlationId:r})}verbose(t,r){this.logMessage(t,{logLevel:e.LogLevel.Verbose,containsPii:!1,correlationId:r})}verbosePii(t,r){this.logMessage(t,{logLevel:e.LogLevel.Verbose,containsPii:!0,correlationId:r})}trace(t,r){this.logMessage(t,{logLevel:e.LogLevel.Trace,containsPii:!1,correlationId:r})}tracePii(t,r){this.logMessage(t,{logLevel:e.LogLevel.Trace,containsPii:!0,correlationId:r})}isPiiLoggingEnabled(){return this.piiLoggingEnabled||!1}}const mr="@azure/msal-common",fr="16.4.1",yr={None:"none",AzurePublic:"https://login.microsoftonline.com",AzurePpe:"https://login.windows-ppe.net",AzureChina:"https://login.chinacloudapi.cn",AzureGermany:"https://login.microsoftonline.de",AzureUsGovernment:"https://login.microsoftonline.us"};function wr(e,t){return!!e&&!!t&&e===t.split(".")[1]}function Ir(e,t,r,n){if(n){const{oid:t,sub:r,tid:o,name:i,tfp:s,acr:a,preferred_username:c,upn:h,login_hint:l}=n,d=o||s||a||"";return{tenantId:d,localAccountId:t||r||"",name:i,username:c||h||"",loginHint:l,isHomeTenant:wr(d,e)}}return{tenantId:r,localAccountId:t,username:"",isHomeTenant:wr(r,e)}}function Cr(e,t,r,n){let o=e;if(t){const{isHomeTenant:r,...n}=t;o={...e,...n}}if(r){const{isHomeTenant:t,...i}=Ir(e.homeAccountId,e.localAccountId,e.tenantId,r);return o={...o,...i,idTokenClaims:r,idToken:n},o}return o}function vr(e,t){const r=function(e){if(!e)throw be(Qe);const t=/^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/.exec(e);if(!t||t.length<4)throw be(Ge);return t[2]}(e);try{const e=t(r);return JSON.parse(e)}catch(e){throw be(Ge)}}function kr(e){if(!e.signin_state)return!1;const t=["kmsi","dvc_dmjd"];return e.signin_state.some((e=>t.includes(e.trim().toLowerCase())))}function Tr(e,t){if(0===t||Date.now()-3e5>e+t)throw be(it)}class br{get urlString(){return this._urlString}constructor(e){if(this._urlString=e,!this._urlString)throw ve(Ee);e.includes("#")||(this._urlString=br.canonicalizeUri(e))}static canonicalizeUri(e){if(e){let t=e.toLowerCase();return ke.endsWith(t,"?")?t=t.slice(0,-1):ke.endsWith(t,"?/")&&(t=t.slice(0,-2)),ke.endsWith(t,"/")||(t+="/"),t}return e}validateAsUri(){let e;try{e=this.getUrlComponents()}catch(e){throw ve(_e)}if(!e.HostNameAndPort||!e.PathSegments)throw ve(_e);if(!e.Protocol||"https:"!==e.Protocol.toLowerCase())throw ve(Se)}static appendQueryString(e,t){return t?e.indexOf("?")<0?`${e}?${t}`:`${e}&${t}`:e}static removeHashFromUrl(e){return br.canonicalizeUri(e.split("#")[0])}replaceTenantPath(e){const t=this.getUrlComponents(),r=t.PathSegments;return!e||0===r.length||r[0]!==A&&r[0]!==S||(r[0]=e),br.constructAuthorityUriFromObject(t)}getUrlComponents(){const e=RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"),t=this.urlString.match(e);if(!t)throw ve(_e);const r={Protocol:t[1],HostNameAndPort:t[4],AbsolutePath:t[5],QueryString:t[7]};let n=r.AbsolutePath.split("/");return n=n.filter((e=>e&&e.length>0)),r.PathSegments=n,r.QueryString&&r.QueryString.endsWith("/")&&(r.QueryString=r.QueryString.substring(0,r.QueryString.length-1)),r}static getDomainFromUrl(e){const t=RegExp("^([^:/?#]+://)?([^/?#]*)"),r=e.match(t);if(!r)throw ve(_e);return r[2]}static getAbsoluteUrl(e,t){if("/"===e[0]){const r=new br(t).getUrlComponents();return r.Protocol+"//"+r.HostNameAndPort+e}return e}static constructAuthorityUriFromObject(e){return new br(e.Protocol+"//"+e.HostNameAndPort+"/"+e.PathSegments.join("/"))}}const Ar={endpointMetadata:[{host:"login.microsoftonline.com"},{host:"login.chinacloudapi.cn",issuerHost:"login.partner.microsoftonline.cn"},{host:"login.microsoftonline.us"},{host:"login.sovcloud-identity.fr"},{host:"login.sovcloud-identity.de"},{host:"login.sovcloud-identity.sg"}].reduce(((e,{host:t,issuerHost:r})=>(e[t]=function(e,t){return{token_endpoint:`https://${e}/{tenantid}/oauth2/v2.0/token`,jwks_uri:`https://${e}/{tenantid}/discovery/v2.0/keys`,issuer:`https://${t}/{tenantid}/v2.0`,authorization_endpoint:`https://${e}/{tenantid}/oauth2/v2.0/authorize`,end_session_endpoint:`https://${e}/{tenantid}/oauth2/v2.0/logout`}}(t,r||t),e)),{}),instanceDiscoveryMetadata:{metadata:[{preferred_network:"login.microsoftonline.com",preferred_cache:"login.windows.net",aliases:["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{preferred_network:"login.partner.microsoftonline.cn",preferred_cache:"login.partner.microsoftonline.cn",aliases:["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{preferred_network:"login.microsoftonline.de",preferred_cache:"login.microsoftonline.de",aliases:["login.microsoftonline.de"]},{preferred_network:"login.microsoftonline.us",preferred_cache:"login.microsoftonline.us",aliases:["login.microsoftonline.us","login.usgovcloudapi.net"]},{preferred_network:"login-us.microsoftonline.com",preferred_cache:"login-us.microsoftonline.com",aliases:["login-us.microsoftonline.com"]},{preferred_network:"login.sovcloud-identity.fr",preferred_cache:"login.sovcloud-identity.fr",aliases:["login.sovcloud-identity.fr"]},{preferred_network:"login.sovcloud-identity.de",preferred_cache:"login.sovcloud-identity.de",aliases:["login.sovcloud-identity.de"]},{preferred_network:"login.sovcloud-identity.sg",preferred_cache:"login.sovcloud-identity.sg",aliases:["login.sovcloud-identity.sg"]}]}},Sr=Ar.endpointMetadata,_r=Ar.instanceDiscoveryMetadata,Er=new Set;function Pr(e,t,r,n,o){if(e.trace("1bmquz",t),r&&n){const o=Rr(n,r);if(o)return e.trace("1fotbt",t),o.aliases;e.trace("14avvj",t)}return null}function Rr(e,t){for(let r=0;r{e.aliases.forEach((e=>{Er.add(e)}))}));const Or="cache_quota_exceeded";class Mr extends Error{constructor(e,t){const r=t||ye(e);super(r),Object.setPrototypeOf(this,Mr.prototype),this.name="CacheError",this.errorCode=e,this.errorMessage=r}}function xr(e){return e instanceof Error?"QuotaExceededError"===e.name||"NS_ERROR_DOM_QUOTA_REACHED"===e.name||e.message.includes("exceeded the quota")?new Mr(Or):new Mr(e.name,e.message):new Mr("cache_error_unknown")}function qr(e,t){if(!e)throw be(We);try{const r=t(e);return JSON.parse(r)}catch(e){throw be(Je)}}function Nr(e){if(!e)throw be(Je);const t=e.split(".",2);return{uid:t[0],utid:t.length<2?"":t[1]}}const Ur=0,Lr=1,Hr=2,Dr=3;function Fr(e){if(e){return e.tid||e.tfp||e.acr||null}return null}const Kr={AAD:"AAD",OIDC:"OIDC",EAR:"EAR"};function Br(e){const t=e.tenantProfiles||[];return 0===t.length&&e.realm&&e.localAccountId&&t.push(Ir(e.homeAccountId,e.localAccountId,e.realm)),{homeAccountId:e.homeAccountId,environment:e.environment,tenantId:e.realm,username:e.username,localAccountId:e.localAccountId,loginHint:e.loginHint,name:e.name,nativeAccountId:e.nativeAccountId,authorityType:e.authorityType,tenantProfiles:new Map(t.map((e=>[e.tenantId,e]))),dataBoundary:e.dataBoundary}}function zr(e,t,r){const n=Array.from(e.tenantProfiles?.values()||[]);return 0===n.length&&e.tenantId&&e.localAccountId&&n.push(Ir(e.homeAccountId,e.localAccountId,e.tenantId,e.idTokenClaims)),{authorityType:e.authorityType||U,homeAccountId:e.homeAccountId,localAccountId:e.localAccountId,nativeAccountId:e.nativeAccountId,realm:e.tenantId,environment:e.environment,username:e.username,loginHint:e.loginHint,name:e.name,cloudGraphHostName:t,msGraphHost:r,tenantProfiles:n,dataBoundary:e.dataBoundary}}function jr(e,t,r,n,o,i){if(t!==Lr&&t!==Hr){if(e)try{const t=qr(e,n.base64Decode);if(t.uid&&t.utid)return`${t.uid}.${t.utid}`}catch(e){}r.warning("1ub6wv",o)}return i?.sub||""}class $r{constructor(e,t,r,n,o){this.clientId=e,this.cryptoImpl=t,this.commonLogger=r.clone(mr,fr),this.staticAuthorityOptions=o,this.performanceClient=n}getAllAccounts(e={},t){return this.buildTenantProfiles(this.getAccountsFilteredBy(e,t),t,e)}getAccountInfoFilteredBy(e,t){if(0===Object.keys(e).length||Object.values(e).every((e=>null==e||""===e)))return this.commonLogger.warning("1skb02",t),null;const r=this.getAllAccounts(e,t);if(r.length>1){return r.sort((e=>e.idTokenClaims?-1:1))[0]}return 1===r.length?r[0]:null}getBaseAccountInfo(e,t){const r=this.getAccountsFilteredBy(e,t);return r.length>0?Br(r[0]):null}buildTenantProfiles(e,t,r){return e.flatMap((e=>this.getTenantProfilesFromAccountEntity(e,t,r?.tenantId,r)))}getTenantedAccountInfoByFilter(e,t,r,n,o){let i,s=null;if(o&&!this.tenantProfileMatchesFilter(r,o))return null;const a=this.getIdToken(e,n,t,r.tenantId);return a&&(i=vr(a.secret,this.cryptoImpl.base64Decode),!this.idTokenClaimsMatchTenantProfileFilter(i,o))?null:(s=Cr(e,r,i,a?.secret),s)}getTenantProfilesFromAccountEntity(e,t,r,n){const o=Br(e);let i=o.tenantProfiles||new Map;const s=this.getTokenKeys();if(r){const e=i.get(r);if(!e)return[];i=new Map([[r,e]])}const a=[];return i.forEach((e=>{const r=this.getTenantedAccountInfoByFilter(o,s,e,t,n);r&&a.push(r)})),a}tenantProfileMatchesFilter(e,t){return!(t.localAccountId&&!this.matchLocalAccountIdFromTenantProfile(e,t.localAccountId))&&((!t.name||e.name===t.name)&&(void 0===t.isHomeTenant||e.isHomeTenant===t.isHomeTenant))}idTokenClaimsMatchTenantProfileFilter(e,t){if(t){if(t.localAccountId&&!this.matchLocalAccountIdFromTokenClaims(e,t.localAccountId))return!1;if(t.loginHint&&!this.matchLoginHintFromTokenClaims(e,t.loginHint))return!1;if(t.username&&!this.matchUsername(e.preferred_username,t.username))return!1;if(t.name&&!this.matchName(e,t.name))return!1;if(t.sid&&!this.matchSid(e,t.sid))return!1}return!0}async saveCacheRecord(e,t,r,n,o){if(!e)throw be(ut);try{e.account&&await this.setAccount(e.account,t,r,n),e.idToken&&!1!==o?.idToken&&await this.setIdTokenCredential(e.idToken,t,r),e.accessToken&&!1!==o?.accessToken&&await this.saveAccessToken(e.accessToken,t,r),e.refreshToken&&!1!==o?.refreshToken&&await this.setRefreshTokenCredential(e.refreshToken,t,r),e.appMetadata&&this.setAppMetadata(e.appMetadata,t)}catch(e){throw this.commonLogger?.error("0j476p",t),e instanceof we?e:xr(e)}}async saveAccessToken(e,t,r){const n={clientId:e.clientId,credentialType:e.credentialType,environment:e.environment,homeAccountId:e.homeAccountId,realm:e.realm,tokenType:e.tokenType},o=this.getTokenKeys(),i=Pt.fromString(e.target);o.accessToken.forEach((e=>{if(!this.accessTokenKeyMatchesFilter(e,n,!1))return;const r=this.getAccessTokenCredential(e,t);if(r&&this.credentialMatchesFilter(r,n,t)){Pt.fromString(r.target).intersectingScopeSets(i)&&this.removeAccessToken(e,t)}})),await this.setAccessTokenCredential(e,t,r)}getAccountsFilteredBy(e,t){const r=this.getAccountKeys(),n=[];return r.forEach((r=>{const o=this.getAccount(r,t);if(!o)return;if(e.homeAccountId&&!this.matchHomeAccountId(o,e.homeAccountId))return;if(e.username&&!this.matchUsername(o.username,e.username))return;if(e.environment&&!this.matchEnvironment(o,e.environment,t))return;if(e.realm&&!this.matchRealm(o,e.realm))return;if(e.nativeAccountId&&!this.matchNativeAccountId(o,e.nativeAccountId))return;if(e.authorityType&&!this.matchAuthorityType(o,e.authorityType))return;const i={localAccountId:e?.localAccountId,name:e?.name},s=o.tenantProfiles?.filter((e=>this.tenantProfileMatchesFilter(e,i)));s&&0===s.length||n.push(o)})),n}credentialMatchesFilter(e,t,r){if(t.clientId&&!this.matchClientId(e,t.clientId))return!1;if(t.userAssertionHash&&!this.matchUserAssertionHash(e,t.userAssertionHash))return!1;if("string"==typeof t.homeAccountId&&!this.matchHomeAccountId(e,t.homeAccountId))return!1;if(t.environment&&!this.matchEnvironment(e,t.environment,r))return!1;if(t.realm&&!this.matchRealm(e,t.realm))return!1;if(t.credentialType&&!this.matchCredentialType(e,t.credentialType))return!1;if(t.familyId&&!this.matchFamilyId(e,t.familyId))return!1;if(t.target&&!this.matchTarget(e,t.target))return!1;if(e.credentialType===L.ACCESS_TOKEN_WITH_AUTH_SCHEME){if(t.tokenType&&!this.matchTokenType(e,t.tokenType))return!1;if(t.tokenType===G.SSH&&t.keyId&&!this.matchKeyId(e,t.keyId))return!1}return!0}getAppMetadataFilteredBy(e,t){const r=this.getKeys(),n={};return r.forEach((r=>{if(!this.isAppMetadata(r))return;const o=this.getAppMetadata(r,t);o&&(e.environment&&!this.matchEnvironment(o,e.environment,t)||e.clientId&&!this.matchClientId(o,e.clientId)||(n[r]=o))})),n}getAuthorityMetadataByAlias(e,t){const r=this.getAuthorityMetadataKeys();let n=null;return r.forEach((r=>{if(!this.isAuthorityMetadata(r)||-1===r.indexOf(this.clientId))return;const o=this.getAuthorityMetadata(r,t);o&&-1!==o.aliases.indexOf(e)&&(n=o)})),n}removeAllAccounts(e){this.getAllAccounts({},e).forEach((t=>{this.removeAccount(t,e)}))}removeAccount(e,t){this.removeAccountContext(e,t);this.getAccountKeys().filter((t=>t.includes(e.homeAccountId)&&t.includes(e.environment))).forEach((e=>{this.removeItem(e,t),this.performanceClient.incrementFields({accountsRemoved:1},t)}))}removeAccountContext(e,t){const r=this.getTokenKeys(),n=t=>t.includes(e.homeAccountId)&&t.includes(e.environment);r.idToken.filter(n).forEach((e=>{this.removeIdToken(e,t)})),r.accessToken.filter(n).forEach((e=>{this.removeAccessToken(e,t)})),r.refreshToken.filter(n).forEach((e=>{this.removeRefreshToken(e,t)}))}removeAccessToken(e,t){const r=this.getAccessTokenCredential(e,t);if(r&&(this.removeItem(e,t),this.performanceClient.incrementFields({accessTokensRemoved:1},t),r.credentialType.toLowerCase()===L.ACCESS_TOKEN_WITH_AUTH_SCHEME.toLowerCase()&&r.tokenType===G.POP)){const e=r.keyId;e&&this.cryptoImpl.removeTokenBindingKey(e,t).catch((()=>{this.commonLogger.error("0cx291",t),this.performanceClient?.incrementFields({removeTokenBindingKeyFailure:1},t)}))}}removeAppMetadata(e){return this.getKeys().forEach((t=>{this.isAppMetadata(t)&&this.removeItem(t,e)})),!0}getIdToken(e,t,r,n){this.commonLogger.trace("1drz22",t);const o={homeAccountId:e.homeAccountId,environment:e.environment,credentialType:L.ID_TOKEN,clientId:this.clientId,realm:n},i=this.getIdTokensByFilter(o,t,r),s=i.size;if(s<1)return this.commonLogger.info("1atvtd",t),null;if(s>1){let r=i;if(!n){const n=new Map;i.forEach(((t,r)=>{t.realm===e.tenantId&&n.set(r,t)}));const o=n.size;if(o<1)return this.commonLogger.info("0ooalx",t),i.values().next().value;if(1===o)return this.commonLogger.info("1eq2vc",t),n.values().next().value;r=n}return this.commonLogger.info("1ws328",t),r.forEach(((e,r)=>{this.removeIdToken(r,t)})),this.performanceClient.addFields({multiMatchedID:i.size},t),null}return this.commonLogger.info("1sm769",t),i.values().next().value}getIdTokensByFilter(e,t,r){const n=r&&r.idToken||this.getTokenKeys().idToken,o=new Map;return n.forEach((r=>{if(!this.idTokenKeyMatchesFilter(r,{clientId:this.clientId,...e}))return;const n=this.getIdTokenCredential(r,t);n&&this.credentialMatchesFilter(n,e,t)&&o.set(r,n)})),o}idTokenKeyMatchesFilter(e,t){const r=e.toLowerCase();return(!t.clientId||-1!==r.indexOf(t.clientId.toLowerCase()))&&(!t.homeAccountId||-1!==r.indexOf(t.homeAccountId.toLowerCase()))}removeIdToken(e,t){this.removeItem(e,t)}removeRefreshToken(e,t){this.removeItem(e,t)}getAccessToken(e,t,r,n){const o=t.correlationId;this.commonLogger.trace("1t7hz1",o);const i=Pt.createSearchScopes(t.scopes),s=t.authenticationScheme||G.BEARER,a=s&&s.toLowerCase()!==G.BEARER.toLowerCase()?L.ACCESS_TOKEN_WITH_AUTH_SCHEME:L.ACCESS_TOKEN,c={homeAccountId:e.homeAccountId,environment:e.environment,credentialType:a,clientId:this.clientId,realm:n||e.tenantId,target:i,tokenType:s,keyId:t.sshKid},h=r&&r.accessToken||this.getTokenKeys().accessToken,l=[];h.forEach((e=>{if(this.accessTokenKeyMatchesFilter(e,c,!0)){const t=this.getAccessTokenCredential(e,o);t&&this.credentialMatchesFilter(t,c,o)&&l.push(t)}}));const d=l.length;return d<1?(this.commonLogger.info("1nckna",o),null):d>1?(this.commonLogger.info("1wkfwp",o),l.forEach((e=>{this.removeAccessToken(this.generateCredentialKey(e),o)})),this.performanceClient.addFields({multiMatchedAT:l.length},o),null):(this.commonLogger.info("06yt98",o),l[0])}accessTokenKeyMatchesFilter(e,t,r){const n=e.toLowerCase();if(t.clientId&&-1===n.indexOf(t.clientId.toLowerCase()))return!1;if(t.homeAccountId&&-1===n.indexOf(t.homeAccountId.toLowerCase()))return!1;if(t.realm&&-1===n.indexOf(t.realm.toLowerCase()))return!1;if(t.target){const e=t.target.asArray();for(let t=0;t{if(!this.accessTokenKeyMatchesFilter(r,e,!0))return;const o=this.getAccessTokenCredential(r,t);o&&this.credentialMatchesFilter(o,e,t)&&n.push(o)})),n}getRefreshToken(e,t,r,n){this.commonLogger.trace("0x53vi",r);const o=t?D:void 0,i={homeAccountId:e.homeAccountId,environment:e.environment,credentialType:L.REFRESH_TOKEN,clientId:this.clientId,familyId:o},s=n&&n.refreshToken||this.getTokenKeys().refreshToken,a=[];s.forEach((e=>{if(this.refreshTokenKeyMatchesFilter(e,i)){const t=this.getRefreshTokenCredential(e,r);t&&this.credentialMatchesFilter(t,i,r)&&a.push(t)}}));const c=a.length;return c<1?(this.commonLogger.info("0dlw11",r),null):(c>1&&this.performanceClient.addFields({multiMatchedRT:c},r),this.commonLogger.info("0wcnep",r),a[0])}refreshTokenKeyMatchesFilter(e,t){const r=e.toLowerCase();return(!t.familyId||-1!==r.indexOf(t.familyId.toLowerCase()))&&(!(!t.familyId&&t.clientId&&-1===r.indexOf(t.clientId.toLowerCase()))&&(!t.homeAccountId||-1!==r.indexOf(t.homeAccountId.toLowerCase())))}readAppMetadataFromCache(e,t){const r={environment:e,clientId:this.clientId},n=this.getAppMetadataFilteredBy(r,t),o=Object.keys(n).map((e=>n[e])),i=o.length;if(i<1)return null;if(i>1)throw be(st);return o[0]}isAppMetadataFOCI(e,t){const r=this.readAppMetadataFromCache(e,t);return!(!r||r.familyId!==D)}matchHomeAccountId(e,t){return!("string"!=typeof e.homeAccountId||t!==e.homeAccountId)}matchLocalAccountIdFromTokenClaims(e,t){return t===(e.oid||e.sub)}matchLocalAccountIdFromTenantProfile(e,t){return e.localAccountId===t}matchName(e,t){return!(t.toLowerCase()!==e.name?.toLowerCase())}matchUsername(e,t){return!(!e||"string"!=typeof e||t?.toLowerCase()!==e.toLowerCase())}matchUserAssertionHash(e,t){return!(!e.userAssertionHash||t!==e.userAssertionHash)}matchEnvironment(e,t,r){if(this.staticAuthorityOptions){const n=function(e,t,r){let n;const o=e.canonicalAuthority;if(o){const i=new br(o).getUrlComponents().HostNameAndPort;n=Pr(t,r,i,e.cloudDiscoveryMetadata?.metadata)||Pr(t,r,i,_r.metadata)||e.knownAuthorities}return n||[]}(this.staticAuthorityOptions,this.commonLogger,r);if(n.includes(t)&&n.includes(e.environment))return!0}const n=this.getAuthorityMetadataByAlias(t,r);return!!(n&&n.aliases.indexOf(e.environment)>-1)}matchCredentialType(e,t){return e.credentialType&&t.toLowerCase()===e.credentialType.toLowerCase()}matchClientId(e,t){return!(!e.clientId||t!==e.clientId)}matchFamilyId(e,t){return!(!e.familyId||t!==e.familyId)}matchRealm(e,t){return!(e.realm?.toLowerCase()!==t.toLowerCase())}matchNativeAccountId(e,t){return!(!e.nativeAccountId||t!==e.nativeAccountId)}matchLoginHintFromTokenClaims(e,t){return e.login_hint===t||(e.preferred_username===t||e.upn===t)}matchSid(e,t){return e.sid===t}matchAuthorityType(e,t){return!(!e.authorityType||t.toLowerCase()!==e.authorityType.toLowerCase())}matchTarget(e,t){if(e.credentialType!==L.ACCESS_TOKEN&&e.credentialType!==L.ACCESS_TOKEN_WITH_AUTH_SCHEME||!e.target)return!1;return Pt.fromString(e.target).containsScopeSet(t)}matchTokenType(e,t){return!(!e.tokenType||e.tokenType!==t)}matchKeyId(e,t){return!(!e.keyId||e.keyId!==t)}isAppMetadata(e){return-1!==e.indexOf(H)}isAuthorityMetadata(e){return-1!==e.indexOf(F)}generateAuthorityMetadataCacheKey(e){return`${F}-${this.clientId}-${e}`}static toObject(e,t){for(const r in t)e[r]=t[r];return e}}class Jr extends $r{async setAccount(){throw be(bt)}getAccount(){throw be(bt)}async setIdTokenCredential(){throw be(bt)}getIdTokenCredential(){throw be(bt)}async setAccessTokenCredential(){throw be(bt)}getAccessTokenCredential(){throw be(bt)}async setRefreshTokenCredential(){throw be(bt)}getRefreshTokenCredential(){throw be(bt)}setAppMetadata(){throw be(bt)}getAppMetadata(){throw be(bt)}setServerTelemetry(){throw be(bt)}getServerTelemetry(){throw be(bt)}setAuthorityMetadata(){throw be(bt)}getAuthorityMetadata(){throw be(bt)}getAuthorityMetadataKeys(){throw be(bt)}setThrottlingCache(){throw be(bt)}getThrottlingCache(){throw be(bt)}removeItem(){throw be(bt)}getKeys(){throw be(bt)}getAccountKeys(){throw be(bt)}getTokenKeys(){throw be(bt)}generateCredentialKey(){throw be(bt)}generateAccountKey(){throw be(bt)}}const Wr=1,Gr=2,Qr="ext.",Vr=new Set(["accessTokenSize","durationMs","idTokenSize","matsSilentStatus","matsHttpStatus","refreshTokenSize","startTimeMs","status","multiMatchedAT","multiMatchedID","multiMatchedRT","unencryptedCacheCount","encryptedCacheExpiredCount","oldAccountCount","oldAccessCount","oldIdCount","oldRefreshCount","currAccountCount","currAccessCount","currIdCount","currRefreshCount","expiredCacheRemovedCount","upgradedCacheCount","cacheMatchedAccounts","networkRtt","redirectBridgeTimeoutMs","redirectBridgeMessageVersion"]);class Xr{generateId(){return"callback-id"}startMeasurement(e,t){return{end:()=>null,discard:()=>{},add:()=>{},increment:()=>{},event:{eventId:this.generateId(),status:Wr,authority:"",libraryName:"",libraryVersion:"",clientId:"",name:e,startTimeMs:Date.now(),correlationId:t||""}}}endMeasurement(){return null}discardMeasurements(){}removePerformanceCallback(){return!0}addPerformanceCallback(){return""}emitEvents(){}addFields(){}incrementFields(){}cacheEventByCorrelationId(){}}const Yr={tokenRenewalOffsetSeconds:300,preventCorsPreflight:!1},Zr={loggerCallback:()=>{},piiLoggingEnabled:!1,logLevel:e.LogLevel.Info,correlationId:""},en={async sendGetRequestAsync(){throw be(bt)},async sendPostRequestAsync(){throw be(bt)}},tn={sku:"msal.js.common",version:fr,cpu:"",os:""},rn={clientSecret:"",clientAssertion:void 0},nn={azureCloudInstance:yr.None,tenant:`${r}`},on={application:{appName:"",appVersion:""}};function sn({authOptions:e,systemOptions:t,loggerOptions:r,storageInterface:n,networkInterface:o,cryptoInterface:i,clientCredentials:s,libraryInfo:a,telemetry:c,serverTelemetryManager:h,persistencePlugin:l,serializableCache:d}){const u={...Zr,...r};return{authOptions:(g=e,{clientCapabilities:[],azureCloudOptions:nn,instanceAware:!1,isMcp:!1,...g}),systemOptions:{...Yr,...t},loggerOptions:u,storageInterface:n||new Jr(e.clientId,lr,new pr(u),new Xr),networkInterface:o||en,cryptoInterface:i||lr,clientCredentials:s||rn,libraryInfo:{...tn,...a},telemetry:{...on,...c},serverTelemetryManager:h||null,persistencePlugin:l||null,serializableCache:d||null};var g}function an(e){return e.authOptions.authority.options.protocolMode===Kr.OIDC}class cn{constructor(e,t){this.cache=e,this.hasChanged=t}get cacheHasChanged(){return this.hasChanged}get tokenCache(){return this.cache}}function hn(){return Math.round((new Date).getTime()/1e3)}function ln(e){return e.getTime()/1e3}function dn(e){return e?new Date(1e3*Number(e)):new Date}function un(e,t){const r=Number(e)||0;return hn()+t>r}function gn(e,t){const r=Number(e)+24*t*60*60*1e3;return Date.now()>r}function pn(e){return Number(e)>hn()}function mn(e,t,r,n,o){return{credentialType:L.ID_TOKEN,homeAccountId:e,environment:t,clientId:n,secret:r,realm:o,lastUpdatedAt:Date.now().toString()}}function fn(e,t,r,n,o,i,s,a,c,h,l,d,u){const g={homeAccountId:e,credentialType:L.ACCESS_TOKEN,secret:r,cachedAt:hn().toString(),expiresOn:s.toString(),extendedExpiresOn:a.toString(),environment:t,clientId:n,realm:o,target:i,tokenType:l||G.BEARER,lastUpdatedAt:Date.now().toString()};if(d&&(g.userAssertionHash=d),h&&(g.refreshOn=h.toString()),g.tokenType?.toLowerCase()!==G.BEARER.toLowerCase())switch(g.credentialType=L.ACCESS_TOKEN_WITH_AUTH_SCHEME,g.tokenType){case G.POP:const e=vr(r,c);if(!e?.cnf?.kid)throw be(yt);g.keyId=e.cnf.kid;break;case G.SSH:g.keyId=u}return g}function yn(e,t,r,n,o,i,s){const a={credentialType:L.REFRESH_TOKEN,homeAccountId:e,environment:t,clientId:n,secret:r,lastUpdatedAt:Date.now().toString()};return i&&(a.userAssertionHash=i),o&&(a.familyId=o),s&&(a.expiresOn=s.toString()),a}function wn(e){return e.hasOwnProperty("homeAccountId")&&e.hasOwnProperty("environment")&&e.hasOwnProperty("credentialType")&&e.hasOwnProperty("clientId")&&e.hasOwnProperty("secret")}function In(e){return!!e&&(wn(e)&&e.hasOwnProperty("realm")&&e.hasOwnProperty("target")&&(e.credentialType===L.ACCESS_TOKEN||e.credentialType===L.ACCESS_TOKEN_WITH_AUTH_SCHEME))}function Cn(e){return!!e&&(wn(e)&&e.credentialType===L.REFRESH_TOKEN)}function vn(){return hn()+86400}function kn(e,t,r){e.authorization_endpoint=t.authorization_endpoint,e.token_endpoint=t.token_endpoint,e.end_session_endpoint=t.end_session_endpoint,e.issuer=t.issuer,e.endpointsFromNetwork=r,e.jwks_uri=t.jwks_uri}function Tn(e,t,r){e.aliases=t.aliases,e.preferred_cache=t.preferred_cache,e.preferred_network=t.preferred_network,e.aliasesFromNetwork=r}function bn(e){return e.expiresAt<=hn()}const An="networkClientSendPostRequestAsync",Sn="refreshTokenClientAcquireTokenWithCachedRefreshToken",_n="getAuthCodeUrl",En="handleCodeResponseFromServer",Pn="popTokenGenerateCnf",Rn="handleServerTokenResponse",On="authorityUpdateMetadataWithRegionalInformation",Mn="regionDiscoveryGetRegionFromIMDS",xn=(e,t,r,n,o)=>(...i)=>{r.trace("1plfzx",o);const s=n.startMeasurement(t,o);o&&n.incrementFields({[`ext.${t}CallCount`]:1},o);try{const t=e(...i);return s.end({success:!0}),r.trace("1g8n6a",o),t}catch(e){r.trace("0cfd8i",o);try{r.trace(JSON.stringify(e),o)}catch(e){r.trace("00dty7",o)}throw s.end({success:!1},e),e}},qn=(e,t,r,n,o)=>(...i)=>{r.trace("1plfzx",o);const s=n.startMeasurement(t,o);return o&&n.incrementFields({[`ext.${t}CallCount`]:1},o),e(...i).then((e=>(r.trace("1g8n6a",o),s.end({success:!0}),e))).catch((e=>{r.trace("0cfd8i",o);try{r.trace(JSON.stringify(e),o)}catch(e){r.trace("00dty7",o)}throw s.end({success:!1},e),e}))},Nn="sw";class Un{constructor(e,t){this.cryptoUtils=e,this.performanceClient=t}async generateCnf(e,t){const r=await qn(this.generateKid.bind(this),Pn,t,this.performanceClient,e.correlationId)(e),n=this.cryptoUtils.base64UrlEncode(JSON.stringify(r));return{kid:r.kid,reqCnfString:n}}async generateKid(e){return{kid:await this.cryptoUtils.getPublicKeyThumbprint(e),xms_ksl:Nn}}async signPopToken(e,t,r){return this.signPayload(e,t,r)}async signPayload(e,t,r,n){const{resourceRequestMethod:o,resourceRequestUri:i,shrClaims:s,shrNonce:a,shrOptions:c}=r,h=i?new br(i):void 0,l=h?.getUrlComponents();return this.cryptoUtils.signJwt({at:e,ts:hn(),m:o?.toUpperCase(),u:l?.HostNameAndPort,nonce:a||this.cryptoUtils.createNewGuid(),p:l?.AbsolutePath,q:l?.QueryString?[[],l.QueryString]:void 0,client_claims:s||void 0,...n},t,c,r.correlationId)}}const Ln="no_tokens_found",Hn="native_account_unavailable",Dn="refresh_token_expired",Fn="ux_not_allowed",Kn="interaction_required",Bn="consent_required",zn="login_required",jn="bad_token",$n="interrupted_user";var Jn=Object.freeze({__proto__:null,badToken:jn,consentRequired:Bn,interactionRequired:Kn,interruptedUser:$n,loginRequired:zn,nativeAccountUnavailable:Hn,noTokensFound:Ln,refreshTokenExpired:Dn,uxNotAllowed:Fn});const Wn=[Kn,Bn,zn,jn,Fn,$n],Gn=["message_only","additional_action","basic_action","user_password_expired","consent_required","bad_token","ux_not_allowed","interrupted_user"];class Qn extends we{constructor(e,t,r,n,o,i,s,a){super(e,t,r),Object.setPrototypeOf(this,Qn.prototype),this.timestamp=n||"",this.traceId=o||"",this.correlationId=i||"",this.claims=s||"",this.name="InteractionRequiredAuthError",this.errorNo=a}}function Vn(e,t,r){const n=!!e&&Wn.indexOf(e)>-1,o=!!r&&Gn.indexOf(r)>-1,i=!!t&&Wn.some((e=>t.indexOf(e)>-1));return n||i||o}function Xn(e,t){return new Qn(e,t)}class Yn extends we{constructor(e,t,r,n,o){super(e,t,r),this.name="ServerError",this.errorNo=n,this.status=o,Object.setPrototypeOf(this,Yn.prototype)}}function Zn(e,t,r){const n=function(e,t){if(!e)throw be(mt);const r={id:e.createNewGuid()};t&&(r.meta=t);const n=JSON.stringify(r);return e.base64Encode(n)}(e,r);return t?`${n}|${t}`:n}function eo(e,t){if(!e)throw be(mt);if(!t)throw be(et);try{const r=t.split("|"),n=r[0],o=r.length>1?r.slice(1).join("|"):"",i=e(n);return{userRequestState:o||"",libraryState:JSON.parse(i)}}catch(e){throw be(et)}}class to{constructor(e,t,r,n,o,i,s){this.clientId=e,this.cacheStorage=t,this.cryptoObj=r,this.logger=n,this.performanceClient=o,this.serializableCache=i,this.persistencePlugin=s}validateTokenResponse(e,t,r){if(e.error||e.error_description||e.suberror){const n=`Error(s): ${e.error_codes||h} - Timestamp: ${e.timestamp||h} - Description: ${e.error_description||h} - Correlation ID: ${e.correlation_id||h} - Trace ID: ${e.trace_id||h}`,o=e.error_codes?.length?e.error_codes[0]:void 0,i=new Yn(e.error,n,e.suberror,o,e.status);if(r&&e.status&&e.status>=500&&e.status<=599)return void this.logger.warning("16ks7j",t);if(r&&e.status&&e.status>=400&&e.status<=499)return void this.logger.warning("0g61x3",t);if(Vn(e.error,e.error_description,e.suberror))throw new Qn(e.error,e.error_description,e.suberror,e.timestamp||"",e.trace_id||"",e.correlation_id||"",e.claims||"",o);throw i}}async handleServerTokenResponse(e,t,r,n,o,i,s,a,c,h){let l,d;if(e.id_token){if(l=vr(e.id_token||"",this.cryptoObj.base64Decode),i&&i.nonce&&l.nonce!==i.nonce)throw be(nt);if(n.maxAge||0===n.maxAge){const e=l.auth_time;if(!e)throw be(ot);Tr(e,n.maxAge)}}this.homeAccountIdentifier=jr(e.client_info||"",t.authorityType,this.logger,this.cryptoObj,n.correlationId,l),i&&i.state&&(d=eo(this.cryptoObj.base64Decode,i.state)),e.key_id=e.key_id||n.sshKid||void 0;const u=this.generateCacheRecord(e,t,r,n,l,s,i);let g;try{if(this.persistencePlugin&&this.serializableCache&&(this.logger.verbose("0jbz5k",n.correlationId),g=new cn(this.serializableCache,!0),await this.persistencePlugin.beforeCacheAccess(g)),a&&!c&&u.account){if(this.cacheStorage.getAllAccounts({homeAccountId:u.account.homeAccountId,environment:u.account.environment},n.correlationId).length<1)return this.logger.warning("1gmt66",n.correlationId),this.performanceClient?.addFields({acntLoggedOut:!0},n.correlationId),await to.generateAuthenticationResult(this.cryptoObj,t,u,!1,n,this.performanceClient,l,d,void 0,h)}await this.cacheStorage.saveCacheRecord(u,n.correlationId,kr(l||{}),o,n.storeInCache)}finally{this.persistencePlugin&&this.serializableCache&&g&&(this.logger.verbose("1bh17u",n.correlationId),await this.persistencePlugin.afterCacheAccess(g))}return to.generateAuthenticationResult(this.cryptoObj,t,u,!1,n,this.performanceClient,l,d,e,h)}generateCacheRecord(e,t,r,n,o,i,s){const a=t.getPreferredCache();if(!a)throw be(gt);const c=Fr(o);let h,l;e.id_token&&o&&(h=mn(this.homeAccountIdentifier,a,e.id_token,this.clientId,c||""),l=ro(this.cacheStorage,t,this.homeAccountIdentifier,this.cryptoObj.base64Decode,n.correlationId,o,e.client_info,a,c,s,void 0,this.logger,this.performanceClient));let d=null;if(e.access_token){const o=e.scope?Pt.fromString(e.scope):new Pt(n.scopes||[]),s=("string"==typeof e.expires_in?parseInt(e.expires_in,10):e.expires_in)||0,h=("string"==typeof e.ext_expires_in?parseInt(e.ext_expires_in,10):e.ext_expires_in)||0,l=("string"==typeof e.refresh_in?parseInt(e.refresh_in,10):e.refresh_in)||void 0,u=r+s,g=u+h,p=l&&l>0?r+l:void 0;d=fn(this.homeAccountIdentifier,a,e.access_token,this.clientId,c||t.tenant||"",o.printScopes(),u,g,this.cryptoObj.base64Decode,p,e.token_type,i,e.key_id);const m=n.resource||null;m&&(d.resource=m)}let u=null;if(e.refresh_token){let t;if(e.refresh_token_expires_in){t=r+("string"==typeof e.refresh_token_expires_in?parseInt(e.refresh_token_expires_in,10):e.refresh_token_expires_in),this.performanceClient?.addFields({ntwkRtExpiresOnSeconds:t},n.correlationId)}u=yn(this.homeAccountIdentifier,a,e.refresh_token,this.clientId,e.foci,i,t)}let g=null;return e.foci&&(g={clientId:this.clientId,environment:a,familyId:e.foci}),{account:l,idToken:h,accessToken:d,refreshToken:u,appMetadata:g}}static async generateAuthenticationResult(e,t,r,n,o,i,s,a,c,h){let l,d,u="",g=[],p=null,m="";if(r.accessToken){if(r.accessToken.tokenType!==G.POP||o.popKid)u=r.accessToken.secret;else{const t=new Un(e,i),{secret:n,keyId:s}=r.accessToken;if(!s)throw be(vt);u=await t.signPopToken(n,s,o)}g=Pt.fromString(r.accessToken.target).asArray(),p=dn(r.accessToken.expiresOn),l=dn(r.accessToken.extendedExpiresOn),r.accessToken.refreshOn&&(d=dn(r.accessToken.refreshOn))}r.appMetadata&&(m=r.appMetadata.familyId===D?D:"");const f=s?.oid||s?.sub||"",y=s?.tid||"";c?.spa_accountid&&r.account&&(r.account.nativeAccountId=c?.spa_accountid);const w=r.account?Cr(Br(r.account),void 0,s,r.idToken?.secret):null;return{authority:t.canonicalAuthority,uniqueId:f,tenantId:y,scopes:g,account:w,idToken:r?.idToken?.secret||"",idTokenClaims:s||{},accessToken:u,fromCache:n,expiresOn:p,extExpiresOn:l,refreshOn:d,correlationId:o.correlationId,requestId:h||"",familyId:m,tokenType:r.accessToken?.tokenType||"",state:a?a.userRequestState:"",cloudGraphHostName:r.account?.cloudGraphHostName||"",msGraphHost:r.account?.msGraphHost||"",code:c?.spa_code,fromPlatformBroker:!1}}}function ro(e,t,r,n,o,i,s,a,c,h,l,d,u){d?.verbose("09jz0t",o);const g=a||t.getPreferredCache(),p=e.getAccountsFilteredBy({homeAccountId:r,environment:g},o);u?.addFields({cacheMatchedAccounts:p.length},o),p.length>1&&d?.warning("0x7ad1",o);const m=(1===p.length?p[0]:null)||function(e,t,r){let n,o,i;n=t.authorityType===Lr?"ADFS":t.protocolMode===Kr.OIDC?U:"MSSTS",e.clientInfo&&r&&(o=qr(e.clientInfo,r),o.xms_tdbr&&(i="EU"===o.xms_tdbr?"EU":"None"));const s=e.environment||t&&t.getPreferredCache();if(!s)throw be(gt);const a=e.idTokenClaims?.preferred_username||e.idTokenClaims?.upn,c=e.idTokenClaims?.emails?e.idTokenClaims.emails[0]:null,h=a||c||"",l=e.idTokenClaims?.login_hint,d=o?.utid||Fr(e.idTokenClaims)||"",u=o?.uid||e.idTokenClaims?.oid||e.idTokenClaims?.sub||"";let g;g=e.tenantProfiles?e.tenantProfiles:[Ir(e.homeAccountId,u,d,e.idTokenClaims)];return{homeAccountId:e.homeAccountId,environment:s,realm:d,localAccountId:u,username:h,authorityType:n,loginHint:l,clientInfo:e.clientInfo,name:e.idTokenClaims?.name||"",lastModificationTime:void 0,lastModificationApp:void 0,cloudGraphHostName:e.cloudGraphHostName,msGraphHost:e.msGraphHost,nativeAccountId:e.nativeAccountId,tenantProfiles:g,dataBoundary:i}}({homeAccountId:r,idTokenClaims:i,clientInfo:s,environment:a,cloudGraphHostName:h?.cloud_graph_host_name,msGraphHost:h?.msgraph_host,nativeAccountId:l},t,n),f=m.tenantProfiles||[],y=c||m.realm;if(y&&!f.find((e=>e.tenantId===y))){const e=Ir(r,m.localAccountId,y,i);f.push(e)}return m.tenantProfiles=f,m}const no="home_account_id",oo="UPN";async function io(e,t,r){if("string"==typeof e)return e;return e({clientId:t,tokenEndpoint:r})}function so(e,t,r){return{clientId:e,authority:t.authority,scopes:t.scopes,homeAccountIdentifier:r,claims:t.claims,authenticationScheme:t.authenticationScheme,resourceRequestMethod:t.resourceRequestMethod,resourceRequestUri:t.resourceRequestUri,shrClaims:t.shrClaims,sshKid:t.sshKid,embeddedClientId:t.embeddedClientId||t.extraParameters?.clientId}}class ao{static generateThrottlingStorageKey(e){return`${Q}.${JSON.stringify(e)}`}static preProcess(e,t,r){const n=ao.generateThrottlingStorageKey(t),o=e.getThrottlingCache(n,r);if(o){if(o.throttleTime=500&&e.status<600}static checkResponseForRetryAfter(e){return!!e.headers&&(e.headers.hasOwnProperty(w)&&(e.status<200||e.status>=300))}static calculateThrottleTime(e){const t=e<=0?0:e,r=Date.now()/1e3;return Math.floor(1e3*Math.min(r+(t||60),r+3600))}static removeThrottle(e,t,r,n){const o=so(t,r,n),i=this.generateThrottlingStorageKey(o);e.removeItem(i,r.correlationId)}}class co extends we{constructor(e,t,r){super(e.errorCode,e.errorMessage,e.subError),Object.setPrototypeOf(this,co.prototype),this.name="NetworkError",this.error=e,this.httpStatus=t,this.responseHeaders=r}}function ho(e,t,r,n){return e.errorMessage=`${e.errorMessage}, additionalErrorInfo: error.name:${n?.name}, error.message:${n?.message}`,new co(e,t,r)}function lo(e,t,r){const n={};if(n[f]="application/x-www-form-urlencoded;charset=utf-8",!t&&r)switch(r.type){case no:try{const e=Nr(r.credential);n[I]=`Oid:${e.uid}@${e.utid}`}catch(t){e.verbose("1qhtee","")}break;case oo:n[I]=`UPN: ${r.credential}`}return n}function uo(e,t,r,n){const o=new Map;return e.embeddedClientId&&nr(o,t,r),e.extraQueryParameters&&Xt(o,e.extraQueryParameters),Ft(o,e.correlationId),Rt(o,e.correlationId,n),cr(o)}async function go(e,t,r,n,o,i,s,a,c,h){const l=await async function(e,t,r,n,o,i,s,a){let c;ao.preProcess(o,e,n);try{c=await qn(i.sendPostRequestAsync.bind(i),An,s,a,n)(t,r);const e=c.headers||{};a?.addFields({refreshTokenSize:c.body.refresh_token?.length||0,httpVerToken:e[T]||"",requestId:e[k]||""},n)}catch(e){if(e instanceof co){const t=e.responseHeaders;throw t&&a?.addFields({httpVerToken:t[T]||"",requestId:t[k]||"",contentTypeHeader:t[f]||void 0,contentLengthHeader:t[y]||void 0,httpStatus:e.httpStatus},n),e.error}throw e instanceof we?e:be(Xe)}return ao.postProcess(o,e,c,n),c}(n,e,{body:t,headers:r},o,i,s,a,c);return h&&l.status<500&&429!==l.status&&h.clearTelemetryCache(),l}class po{constructor(e,t,r,n){this.networkInterface=e,this.logger=t,this.performanceClient=r,this.correlationId=n}async detectRegion(e,t){let r=e;if(r)t.region_source=X;else{const e=po.IMDS_OPTIONS;try{const n=await qn(this.getRegionFromIMDS.bind(this),Mn,this.logger,this.performanceClient,this.correlationId)("2020-06-01",e);if(200===n.status&&(r=n.body,t.region_source=Y),400===n.status){const n=await qn(this.getCurrentVersion.bind(this),"regionDiscoveryGetCurrentVersion",this.logger,this.performanceClient,this.correlationId)(e);if(!n)return t.region_source=V,null;const o=await qn(this.getRegionFromIMDS.bind(this),Mn,this.logger,this.performanceClient,this.correlationId)(n,e);200===o.status&&(r=o.body,t.region_source=Y)}}catch(e){return t.region_source=V,null}}return r||(t.region_source=V),r||null}async getRegionFromIMDS(e,t){return this.networkInterface.sendGetRequestAsync(`${l}?api-version=${e}&format=text`,t,2e3)}async getCurrentVersion(e){try{const t=await this.networkInterface.sendGetRequestAsync(`${l}?format=json`,e);return 400===t.status&&t.body&&t.body["newest-versions"]&&t.body["newest-versions"].length>0?t.body["newest-versions"][0]:null}catch(e){return null}}}po.IMDS_OPTIONS={headers:{Metadata:"true"}};class mo{constructor(e,t,r,n,o,i,s,a){this.canonicalAuthority=e,this._canonicalAuthority.validateAsUri(),this.networkInterface=t,this.cacheManager=r,this.authorityOptions=n,this.regionDiscoveryMetadata={region_used:void 0,region_source:void 0,region_outcome:void 0},this.logger=o,this.performanceClient=s,this.correlationId=i,this.managedIdentity=a||!1,this.regionDiscovery=new po(t,this.logger,this.performanceClient,this.correlationId)}getAuthorityType(e){if(e.HostNameAndPort.endsWith(o))return Dr;const t=e.PathSegments;if(t.length)switch(t[0].toLowerCase()){case"adfs":return Lr;case"dstsv2":return Hr}return Ur}get authorityType(){return this.getAuthorityType(this.canonicalAuthorityUrlComponents)}get protocolMode(){return this.authorityOptions.protocolMode}get options(){return this.authorityOptions}get canonicalAuthority(){return this._canonicalAuthority.urlString}set canonicalAuthority(e){this._canonicalAuthority=new br(e),this._canonicalAuthority.validateAsUri(),this._canonicalAuthorityUrlComponents=null}get canonicalAuthorityUrlComponents(){return this._canonicalAuthorityUrlComponents||(this._canonicalAuthorityUrlComponents=this._canonicalAuthority.getUrlComponents()),this._canonicalAuthorityUrlComponents}get hostnameAndPort(){return this.canonicalAuthorityUrlComponents.HostNameAndPort.toLowerCase()}get tenant(){return this.canonicalAuthorityUrlComponents.PathSegments[0]}get authorizationEndpoint(){if(this.discoveryComplete())return this.replacePath(this.metadata.authorization_endpoint);throw be(Ve)}get tokenEndpoint(){if(this.discoveryComplete())return this.replacePath(this.metadata.token_endpoint);throw be(Ve)}get deviceCodeEndpoint(){if(this.discoveryComplete())return this.replacePath(this.metadata.token_endpoint.replace("/token","/devicecode"));throw be(Ve)}get endSessionEndpoint(){if(this.discoveryComplete()){if(!this.metadata.end_session_endpoint)throw be(Ct);return this.replacePath(this.metadata.end_session_endpoint)}throw be(Ve)}get selfSignedJwtAudience(){if(this.discoveryComplete())return this.replacePath(this.metadata.issuer);throw be(Ve)}get jwksUri(){if(this.discoveryComplete())return this.replacePath(this.metadata.jwks_uri);throw be(Ve)}canReplaceTenant(e){return 1===e.PathSegments.length&&!mo.reservedTenantDomains.has(e.PathSegments[0])&&this.getAuthorityType(e)===Ur&&this.protocolMode!==Kr.OIDC}replaceTenant(e){return e.replace(/{tenant}|{tenantid}/g,this.tenant)}replacePath(e){let t=e;const r=new br(this.metadata.canonical_authority).getUrlComponents(),n=r.PathSegments;return this.canonicalAuthorityUrlComponents.PathSegments.forEach(((e,o)=>{let i=n[o];if(0===o&&this.canReplaceTenant(r)){const e=new br(this.metadata.authorization_endpoint).getUrlComponents().PathSegments[0];i!==e&&(this.logger.verbose("1q3g2x",this.correlationId),i=e)}e!==i&&(t=t.replace(`/${i}/`,`/${e}/`))})),this.replaceTenant(t)}get defaultOpenIdConfigurationEndpoint(){const e=this.hostnameAndPort;return this.canonicalAuthority.endsWith("v2.0/")||this.authorityType===Lr||this.protocolMode===Kr.OIDC&&!this.isAliasOfKnownMicrosoftAuthority(e)?`${this.canonicalAuthority}.well-known/openid-configuration`:`${this.canonicalAuthority}v2.0/.well-known/openid-configuration`}discoveryComplete(){return!!this.metadata}async resolveEndpointsAsync(){const e=this.getCurrentMetadataEntity(),t=await qn(this.updateCloudDiscoveryMetadata.bind(this),"authorityUpdateCloudDiscoveryMetadata",this.logger,this.performanceClient,this.correlationId)(e);this.canonicalAuthority=this.canonicalAuthority.replace(this.hostnameAndPort,e.preferred_network);const r=await qn(this.updateEndpointMetadata.bind(this),"authorityUpdateEndpointMetadata",this.logger,this.performanceClient,this.correlationId)(e);this.updateCachedMetadata(e,t,{source:r}),this.performanceClient?.addFields({cloudDiscoverySource:t,authorityEndpointSource:r},this.correlationId)}getCurrentMetadataEntity(){let e=this.cacheManager.getAuthorityMetadataByAlias(this.hostnameAndPort,this.correlationId);return e||(e={aliases:[],preferred_cache:this.hostnameAndPort,preferred_network:this.hostnameAndPort,canonical_authority:this.canonicalAuthority,authorization_endpoint:"",token_endpoint:"",end_session_endpoint:"",issuer:"",aliasesFromNetwork:!1,endpointsFromNetwork:!1,expiresAt:vn(),jwks_uri:""}),e}updateCachedMetadata(e,t,r){t!==B&&r?.source!==B&&(e.expiresAt=vn(),e.canonical_authority=this.canonicalAuthority);const n=this.cacheManager.generateAuthorityMetadataCacheKey(e.preferred_cache,this.correlationId);this.cacheManager.setAuthorityMetadata(n,e,this.correlationId),this.metadata=e}async updateEndpointMetadata(e){const t=this.updateEndpointMetadataFromLocalSources(e);if(t){if(t.source===j&&this.authorityOptions.azureRegionConfiguration?.azureRegion&&t.metadata){kn(e,await qn(this.updateMetadataWithRegionalInformation.bind(this),On,this.logger,this.performanceClient,this.correlationId)(t.metadata),!1),e.canonical_authority=this.canonicalAuthority}return t.source}let r=await qn(this.getEndpointMetadataFromNetwork.bind(this),"authorityGetEndpointMetadataFromNetwork",this.logger,this.performanceClient,this.correlationId)();if(r)return this.authorityOptions.azureRegionConfiguration?.azureRegion&&(r=await qn(this.updateMetadataWithRegionalInformation.bind(this),On,this.logger,this.performanceClient,this.correlationId)(r)),kn(e,r,!0),z;throw be(Ye,this.defaultOpenIdConfigurationEndpoint)}updateEndpointMetadataFromLocalSources(e){this.logger.verbose("1fi0kc",this.correlationId);const t=this.getEndpointMetadataFromConfig();if(t)return this.logger.verbose("06t0uj",this.correlationId),kn(e,t,!1),{source:K};this.logger.verbose("151k0p",this.correlationId);const r=this.getEndpointMetadataFromHardcodedValues();if(r)return kn(e,r,!1),{source:j,metadata:r};this.logger.verbose("1imop5",this.correlationId);const n=bn(e);return this.isAuthoritySameType(e)&&e.endpointsFromNetwork&&!n?(this.logger.verbose("16uq31",""),{source:B}):(n&&this.logger.verbose("0uoibc",""),null)}isAuthoritySameType(e){return new br(e.canonical_authority).getUrlComponents().PathSegments.length===this.canonicalAuthorityUrlComponents.PathSegments.length}getEndpointMetadataFromConfig(){if(this.authorityOptions.authorityMetadata)try{return JSON.parse(this.authorityOptions.authorityMetadata)}catch(e){throw ve(Ne)}return null}async getEndpointMetadataFromNetwork(){const e={},t=this.defaultOpenIdConfigurationEndpoint;this.logger.verbose("1y65x6",this.correlationId);try{const r=await this.networkInterface.sendGetRequestAsync(t,e),n=function(e){return e.hasOwnProperty("authorization_endpoint")&&e.hasOwnProperty("token_endpoint")&&e.hasOwnProperty("issuer")&&e.hasOwnProperty("jwks_uri")}(r.body);return n?r.body:(this.logger.verbose("1koyv8",this.correlationId),null)}catch(e){return this.logger.verbose("0a9wik",this.correlationId),null}}getEndpointMetadataFromHardcodedValues(){return this.hostnameAndPort in Sr?Sr[this.hostnameAndPort]:null}async updateMetadataWithRegionalInformation(e){const t=this.authorityOptions.azureRegionConfiguration?.azureRegion;if(t){if("TryAutoDetect"!==t)return this.regionDiscoveryMetadata.region_outcome=Z,this.regionDiscoveryMetadata.region_used=t,mo.replaceWithRegionalInformation(e,t);const r=await qn(this.regionDiscovery.detectRegion.bind(this.regionDiscovery),"regionDiscoveryDetectRegion",this.logger,this.performanceClient,this.correlationId)(this.authorityOptions.azureRegionConfiguration?.environmentRegion,this.regionDiscoveryMetadata);if(r)return this.regionDiscoveryMetadata.region_outcome=ee,this.regionDiscoveryMetadata.region_used=r,mo.replaceWithRegionalInformation(e,r);this.regionDiscoveryMetadata.region_outcome=te}return e}async updateCloudDiscoveryMetadata(e){const t=this.updateCloudDiscoveryMetadataFromLocalSources(e);if(t)return t;const r=await qn(this.getCloudDiscoveryMetadataFromNetwork.bind(this),"authorityGetCloudDiscoveryMetadataFromNetwork",this.logger,this.performanceClient,this.correlationId)();if(r)return Tn(e,r,!0),z;throw ve(Ue)}updateCloudDiscoveryMetadataFromLocalSources(e){this.logger.verbose("0jhlgt",this.correlationId),this.logger.verbosePii("1fy7uz",this.correlationId),this.logger.verbosePii("08zabj",this.correlationId),this.logger.verbosePii("1o1kv3",this.correlationId);const t=this.getCloudDiscoveryMetadataFromConfig();if(t)return this.logger.verbose("1nakio",this.correlationId),Tn(e,t,!1),K;this.logger.verbose("1x74aj",this.correlationId);const r=(n=this.hostnameAndPort,Rr(_r.metadata,n));var n;if(r)return this.logger.verbose("0by47c",this.correlationId),Tn(e,r,!1),j;this.logger.verbose("0r2fzy",this.correlationId);const o=bn(e);return this.isAuthoritySameType(e)&&e.aliasesFromNetwork&&!o?(this.logger.verbose("1uffgh",""),B):(o&&this.logger.verbose("0uoibc",""),null)}getCloudDiscoveryMetadataFromConfig(){if(this.authorityType===Dr)return this.logger.verbose("04y84h",this.correlationId),mo.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort);if(this.authorityOptions.cloudDiscoveryMetadata){this.logger.verbose("0gszr3",this.correlationId);try{this.logger.verbose("1iifkx",this.correlationId);const e=Rr(JSON.parse(this.authorityOptions.cloudDiscoveryMetadata).metadata,this.hostnameAndPort);if(this.logger.verbose("0q67e3",""),e)return this.logger.verbose("0hzfao",this.correlationId),e;this.logger.verbose("1ajz3u",this.correlationId)}catch(e){throw this.logger.verbose("1wq5tu",this.correlationId),ve(qe)}}return this.isInKnownAuthorities()?(this.logger.verbose("0mt9al",this.correlationId),mo.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort)):null}async getCloudDiscoveryMetadataFromNetwork(){const e=`${n}${this.canonicalAuthority}oauth2/v2.0/authorize`,t={};let r=null;try{const n=await this.networkInterface.sendGetRequestAsync(e,t);let o,i;if(function(e){return e.hasOwnProperty("tenant_discovery_endpoint")&&e.hasOwnProperty("metadata")}(n.body))o=n.body,i=o.metadata,this.logger.verbosePii("1vglyt",this.correlationId);else{if(!function(e){return e.hasOwnProperty("error")&&e.hasOwnProperty("error_description")}(n.body))return this.logger.error("0768g0",this.correlationId),null;if(this.logger.warning("062uto",this.correlationId),o=n.body,"invalid_instance"===o.error)return this.logger.error("1x90tm",this.correlationId),null;this.logger.warning("0wchdm",this.correlationId),this.logger.warning("1s5mpv",this.correlationId),this.logger.warning("1yhqpw",this.correlationId),i=[]}this.logger.verbose("1lrobr",this.correlationId),r=Rr(i,this.hostnameAndPort)}catch(e){return e instanceof we?this.logger.error("0vwhc7",this.correlationId):this.logger.error("0s2z41",this.correlationId),null}return r||(this.logger.warning("0jp28q",this.correlationId),this.logger.verbose("130sd8",this.correlationId),r=mo.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort)),r}isInKnownAuthorities(){return this.authorityOptions.knownAuthorities.filter((e=>e&&br.getDomainFromUrl(e).toLowerCase()===this.hostnameAndPort)).length>0}static generateAuthority(e,t){let n;if(t&&t.azureCloudInstance!==yr.None){const e=t.tenant?t.tenant:r;n=`${t.azureCloudInstance}/${e}/`}return n||e}static createCloudDiscoveryMetadataFromHost(e){return{preferred_network:e,preferred_cache:e,aliases:[e]}}getPreferredCache(){if(this.managedIdentity)return"login.microsoftonline.com";if(this.discoveryComplete())return this.metadata.preferred_cache;throw be(Ve)}isAlias(e){return this.metadata.aliases.indexOf(e)>-1}isAliasOfKnownMicrosoftAuthority(e){return Er.has(e)}static isPublicCloudAuthority(e){return d.indexOf(e)>=0}static buildRegionalAuthorityString(e,t,r){const n=new br(e);n.validateAsUri();const o=n.getUrlComponents();let i=`${t}.${o.HostNameAndPort}`;this.isPublicCloudAuthority(o.HostNameAndPort)&&(i=`${t}.login.microsoft.com`);const s=br.constructAuthorityUriFromObject({...n.getUrlComponents(),HostNameAndPort:i}).urlString;return r?`${s}?${r}`:s}static replaceWithRegionalInformation(e,t){const r={...e};return r.authorization_endpoint=mo.buildRegionalAuthorityString(r.authorization_endpoint,t),r.token_endpoint=mo.buildRegionalAuthorityString(r.token_endpoint,t),r.end_session_endpoint&&(r.end_session_endpoint=mo.buildRegionalAuthorityString(r.end_session_endpoint,t)),r}static transformCIAMAuthority(e){let t=e;const r=new br(e).getUrlComponents();if(0===r.PathSegments.length&&r.HostNameAndPort.endsWith(o)){t=`${t}${r.HostNameAndPort.split(".")[0]}.onmicrosoft.com`}return t}}function fo(e){return e.endsWith("/")?e:`${e}/`}function yo(e){const t=e.cloudDiscoveryMetadata;let r;if(t)try{r=JSON.parse(t)}catch(e){throw ve(qe)}return{canonicalAuthority:e.authority?fo(e.authority):void 0,knownAuthorities:e.knownAuthorities,cloudDiscoveryMetadata:r}}async function wo(e,t,r,n,o,i,s){const a=mo.transformCIAMAuthority(fo(e)),c=new mo(a,t,r,n,o,i,s);try{return await qn(c.resolveEndpointsAsync.bind(c),"authorityResolveEndpointsAsync",o,s,i)(),c}catch(e){throw be(Ve)}}mo.reservedTenantDomains=new Set(["{tenant}","{tenantid}",A,_,S]);class Io{constructor(e,t){this.includeRedirectUri=!0,this.config=sn(e),this.logger=new pr(this.config.loggerOptions,mr,fr),this.cryptoUtils=this.config.cryptoInterface,this.cacheManager=this.config.storageInterface,this.networkClient=this.config.networkInterface,this.serverTelemetryManager=this.config.serverTelemetryManager,this.authority=this.config.authOptions.authority,this.performanceClient=t,this.oidcDefaultScopes=this.config.authOptions.authority.options.OIDCOptions?.defaultScopes}async acquireToken(e,t,r){if(!e.code)throw be(at);r&&r.cloud_instance_host_name&&await qn(this.updateTokenEndpointAuthority.bind(this),"updateTokenEndpointAuthority",this.logger,this.performanceClient,e.correlationId)(r.cloud_instance_host_name,e.correlationId);const n=hn(),o=await qn(this.executeTokenRequest.bind(this),"authClientExecuteTokenRequest",this.logger,this.performanceClient,e.correlationId)(this.authority,e,this.serverTelemetryManager),i=o.headers?.[k],s=new to(this.config.authOptions.clientId,this.cacheManager,this.cryptoUtils,this.logger,this.performanceClient,this.config.serializableCache,this.config.persistencePlugin);return s.validateTokenResponse(o.body,e.correlationId),qn(s.handleServerTokenResponse.bind(s),Rn,this.logger,this.performanceClient,e.correlationId)(o.body,this.authority,n,e,t,r,void 0,void 0,void 0,i)}getLogoutUri(e){if(!e)throw ve(Me);const t=this.createLogoutUrlQueryString(e);return br.appendQueryString(this.authority.endSessionEndpoint,t)}async executeTokenRequest(e,t,r){const n=uo(t,this.config.authOptions.clientId,this.config.authOptions.redirectUri,this.performanceClient),o=br.appendQueryString(e.tokenEndpoint,n),i=await qn(this.createTokenRequestBody.bind(this),"authClientCreateTokenRequestBody",this.logger,this.performanceClient,t.correlationId)(t);let s;if(t.clientInfo)try{const e=qr(t.clientInfo,this.cryptoUtils.base64Decode);s={credential:`${e.uid}.${e.utid}`,type:no}}catch(e){this.logger.verbose("0wznt3",t.correlationId)}const a=lo(this.logger,this.config.systemOptions.preventCorsPreflight,s||t.ccsCredential),c=so(this.config.authOptions.clientId,t);return qn(go,"authorizationCodeClientExecutePostToTokenEndpoint",this.logger,this.performanceClient,t.correlationId)(o,i,a,c,t.correlationId,this.cacheManager,this.networkClient,this.logger,this.performanceClient,r)}async createTokenRequestBody(e){const t=new Map;if(xt(t,e.embeddedClientId||e.extraParameters?.[ce]||this.config.authOptions.clientId),this.includeRedirectUri)qt(t,e.redirectUri);else if(!e.redirectUri)throw ve(Ae);if(Mt(t,e.scopes,!0,this.oidcDefaultScopes),or(t,e.resource),function(e,t){e.set("code",t)}(t,e.code),Kt(t,this.config.libraryInfo),Bt(t,this.config.telemetry.application),rr(t),this.serverTelemetryManager&&!an(this.config)&&tr(t,this.serverTelemetryManager),e.codeVerifier&&function(e,t){e.set("code_verifier",t)}(t,e.codeVerifier),this.config.clientCredentials.clientSecret&&$t(t,this.config.clientCredentials.clientSecret),this.config.clientCredentials.clientAssertion){const r=this.config.clientCredentials.clientAssertion;Jt(t,await io(r.assertion,this.config.authOptions.clientId,e.resourceRequestUri)),Wt(t,r.assertionType)}if(Gt(t,q),Qt(t),e.authenticationScheme===G.POP){const r=new Un(this.cryptoUtils,this.performanceClient);let n;if(e.popKid)n=this.cryptoUtils.encodeKid(e.popKid);else{n=(await qn(r.generateCnf.bind(r),Pn,this.logger,this.performanceClient,e.correlationId)(e,this.logger)).reqCnfString}Zt(t,n)}else if(e.authenticationScheme===G.SSH){if(!e.sshJwk)throw ve(Le);er(t,e.sshJwk)}let r;if((!ke.isEmptyObj(e.claims)||this.config.authOptions.clientCapabilities&&this.config.authOptions.clientCapabilities.length>0)&&Dt(t,e.claims,this.config.authOptions.clientCapabilities),e.clientInfo)try{const t=qr(e.clientInfo,this.cryptoUtils.base64Decode);r={credential:`${t.uid}.${t.utid}`,type:no}}catch(t){this.logger.verbose("0wznt3",e.correlationId)}else r=e.ccsCredential;if(this.config.systemOptions.preventCorsPreflight&&r)switch(r.type){case no:try{Lt(t,Nr(r.credential))}catch(t){this.logger.verbose("1qhtee",e.correlationId)}break;case oo:Ut(t,r.credential)}return e.embeddedClientId&&nr(t,this.config.authOptions.clientId,this.config.authOptions.redirectUri),e.extraParameters&&Xt(t,e.extraParameters),!e.enableSpaAuthorizationCode||e.extraParameters&&e.extraParameters[ue]||Xt(t,{[ue]:"1"}),Rt(t,e.correlationId,this.performanceClient),cr(t)}createLogoutUrlQueryString(e){const t=new Map;return e.postLogoutRedirectUri&&function(e,t){e.set("post_logout_redirect_uri",t)}(t,e.postLogoutRedirectUri),e.correlationId&&Ft(t,e.correlationId),e.idTokenHint&&function(e,t){e.set("id_token_hint",t)}(t,e.idTokenHint),e.state&&zt(t,e.state),e.logoutHint&&function(e,t){e.set("logout_hint",t)}(t,e.logoutHint),e.extraQueryParameters&&Xt(t,e.extraQueryParameters),this.config.authOptions.instanceAware&&Vt(t),cr(t)}async updateTokenEndpointAuthority(e,t){const r=`https://${e}/${this.authority.tenant}/`,n=await wo(r,this.networkClient,this.cacheManager,this.authority.options,this.logger,t,this.performanceClient);this.authority=n}}class Co{constructor(e,t){this.config=sn(e),this.logger=new pr(this.config.loggerOptions,mr,fr),this.cryptoUtils=this.config.cryptoInterface,this.cacheManager=this.config.storageInterface,this.networkClient=this.config.networkInterface,this.serverTelemetryManager=this.config.serverTelemetryManager,this.authority=this.config.authOptions.authority,this.performanceClient=t}async acquireToken(e,t){const r=hn(),n=await qn(this.executeTokenRequest.bind(this),"refreshTokenClientExecuteTokenRequest",this.logger,this.performanceClient,e.correlationId)(e,this.authority),o=n.headers?.[k],i=new to(this.config.authOptions.clientId,this.cacheManager,this.cryptoUtils,this.logger,this.performanceClient,this.config.serializableCache,this.config.persistencePlugin);return i.validateTokenResponse(n.body,e.correlationId),qn(i.handleServerTokenResponse.bind(i),Rn,this.logger,this.performanceClient,e.correlationId)(n.body,this.authority,r,e,t,void 0,void 0,!0,e.forceCache,o)}async acquireTokenByRefreshToken(e,t){if(!e)throw ve(Oe);if(!e.account)throw be(dt);if(this.cacheManager.isAppMetadataFOCI(e.account.environment,e.correlationId))try{return await qn(this.acquireTokenWithCachedRefreshToken.bind(this),Sn,this.logger,this.performanceClient,e.correlationId)(e,!0,t)}catch(r){const n=r instanceof Qn&&r.errorCode===Ln,o=r instanceof Yn&&"invalid_grant"===r.errorCode&&"client_mismatch"===r.subError;if(n||o)return qn(this.acquireTokenWithCachedRefreshToken.bind(this),Sn,this.logger,this.performanceClient,e.correlationId)(e,!1,t);throw r}return qn(this.acquireTokenWithCachedRefreshToken.bind(this),Sn,this.logger,this.performanceClient,e.correlationId)(e,!1,t)}async acquireTokenWithCachedRefreshToken(e,t,r){const n=xn(this.cacheManager.getRefreshToken.bind(this.cacheManager),"cacheManagerGetRefreshToken",this.logger,this.performanceClient,e.correlationId)(e.account,t,e.correlationId,void 0);if(!n)throw Xn(Ln);if(n.expiresOn){const t=e.refreshTokenExpirationOffsetSeconds||300;if(this.performanceClient?.addFields({cacheRtExpiresOnSeconds:Number(n.expiresOn),rtOffsetSeconds:t},e.correlationId),un(n.expiresOn,t))throw Xn(Dn)}const o={...e,refreshToken:n.secret,authenticationScheme:e.authenticationScheme||G.BEARER,ccsCredential:{credential:e.account.homeAccountId,type:no}};try{return await qn(this.acquireToken.bind(this),"refreshTokenClientAcquireToken",this.logger,this.performanceClient,e.correlationId)(o,r)}catch(t){if(t instanceof Qn&&t.subError===jn){this.logger.verbose("1pg3ap",e.correlationId);const t=this.cacheManager.generateCredentialKey(n);this.cacheManager.removeRefreshToken(t,e.correlationId)}throw t}}async executeTokenRequest(e,t){const r=uo(e,this.config.authOptions.clientId,this.config.authOptions.redirectUri,this.performanceClient),n=br.appendQueryString(t.tokenEndpoint,r),o=await qn(this.createTokenRequestBody.bind(this),"refreshTokenClientCreateTokenRequestBody",this.logger,this.performanceClient,e.correlationId)(e),i=lo(this.logger,this.config.systemOptions.preventCorsPreflight,e.ccsCredential),s=so(this.config.authOptions.clientId,e);return qn(go,"refreshTokenClientExecutePostToTokenEndpoint",this.logger,this.performanceClient,e.correlationId)(n,o,i,s,e.correlationId,this.cacheManager,this.networkClient,this.logger,this.performanceClient,this.serverTelemetryManager)}async createTokenRequestBody(e){const t=new Map;if(xt(t,e.embeddedClientId||e.extraParameters?.[ce]||this.config.authOptions.clientId),e.redirectUri&&qt(t,e.redirectUri),Mt(t,e.scopes,!0,this.config.authOptions.authority.options.OIDCOptions?.defaultScopes),Gt(t,N),Qt(t),Kt(t,this.config.libraryInfo),Bt(t,this.config.telemetry.application),rr(t),this.serverTelemetryManager&&!an(this.config)&&tr(t,this.serverTelemetryManager),function(e,t){e.set("refresh_token",t)}(t,e.refreshToken),this.config.clientCredentials.clientSecret&&$t(t,this.config.clientCredentials.clientSecret),this.config.clientCredentials.clientAssertion){const r=this.config.clientCredentials.clientAssertion;Jt(t,await io(r.assertion,this.config.authOptions.clientId,e.resourceRequestUri)),Wt(t,r.assertionType)}if(e.authenticationScheme===G.POP){const r=new Un(this.cryptoUtils,this.performanceClient);let n;if(e.popKid)n=this.cryptoUtils.encodeKid(e.popKid);else{n=(await qn(r.generateCnf.bind(r),Pn,this.logger,this.performanceClient,e.correlationId)(e,this.logger)).reqCnfString}Zt(t,n)}else if(e.authenticationScheme===G.SSH){if(!e.sshJwk)throw ve(Le);er(t,e.sshJwk)}if((!ke.isEmptyObj(e.claims)||this.config.authOptions.clientCapabilities&&this.config.authOptions.clientCapabilities.length>0)&&Dt(t,e.claims,this.config.authOptions.clientCapabilities),this.config.systemOptions.preventCorsPreflight&&e.ccsCredential)switch(e.ccsCredential.type){case no:try{Lt(t,Nr(e.ccsCredential.credential))}catch(t){this.logger.verbose("1qhtee",e.correlationId)}break;case oo:Ut(t,e.ccsCredential.credential)}return e.embeddedClientId&&nr(t,this.config.authOptions.clientId,this.config.authOptions.redirectUri),e.extraParameters&&Xt(t,{...e.extraParameters}),Rt(t,e.correlationId,this.performanceClient),cr(t)}}class vo{constructor(e,t){this.config=sn(e),this.logger=new pr(this.config.loggerOptions,mr,fr),this.cryptoUtils=this.config.cryptoInterface,this.cacheManager=this.config.storageInterface,this.networkClient=this.config.networkInterface,this.serverTelemetryManager=this.config.serverTelemetryManager,this.authority=this.config.authOptions.authority,this.performanceClient=t}async acquireCachedToken(e){let t=re;if(e.forceRefresh||!ke.isEmptyObj(e.claims))throw this.setCacheOutcome(ne,e.correlationId),be(ft);if(!e.account)throw be(dt);const r=e.account.tenantId||function(e){const t=new br(e).getUrlComponents(),r=t.PathSegments.slice(-1)[0]?.toLowerCase();switch(r){case A:case S:case _:return;default:return r}}(e.authority),n=this.cacheManager.getTokenKeys(),o=this.cacheManager.getAccessToken(e.account,e,n,r);if(!o)throw this.setCacheOutcome(oe,e.correlationId),be(ft);if(pn(o.cachedAt)||un(o.expiresOn,this.config.systemOptions.tokenRenewalOffsetSeconds))throw this.setCacheOutcome(ie,e.correlationId),be(ft);if(e.resource){if(o.resource!==e.resource)throw this.setCacheOutcome(oe,e.correlationId),be(ft)}else o.refreshOn&&un(o.refreshOn,0)&&(t=se);const i=e.authority||this.authority.getPreferredCache(),s={account:this.cacheManager.getAccount(this.cacheManager.generateAccountKey(e.account),e.correlationId),accessToken:o,idToken:this.cacheManager.getIdToken(e.account,e.correlationId,n,r),refreshToken:null,appMetadata:this.cacheManager.readAppMetadataFromCache(i,e.correlationId)};return this.setCacheOutcome(t,e.correlationId),this.config.serverTelemetryManager&&this.config.serverTelemetryManager.incrementCacheHits(),[await qn(this.generateResultFromCacheRecord.bind(this),"silentFlowClientGenerateResultFromCacheRecord",this.logger,this.performanceClient,e.correlationId)(s,e),t]}setCacheOutcome(e,t){this.serverTelemetryManager?.setCacheOutcome(e),this.performanceClient?.addFields({cacheOutcome:e},t),e!==re&&this.logger.info("09ingz",t)}async generateResultFromCacheRecord(e,t){let r;if(e.idToken&&(r=vr(e.idToken.secret,this.config.cryptoInterface.base64Decode)),t.maxAge||0===t.maxAge){const e=r?.auth_time;if(!e)throw be(ot);Tr(e,t.maxAge)}return to.generateAuthenticationResult(this.cryptoUtils,this.authority,e,!0,t,this.performanceClient,r)}}const ko={sendGetRequestAsync:()=>Promise.reject(be(bt)),sendPostRequestAsync:()=>Promise.reject(be(bt))};function To(e,t,r,n){const o=t.correlationId,i=new Map;xt(i,t.embeddedClientId||t.extraQueryParameters?.[ce]||e.clientId);if(Mt(i,[...t.scopes||[],...t.extraScopesToConsent||[]],!0,e.authority.options.OIDCOptions?.defaultScopes),or(i,t.resource),qt(i,t.redirectUri),Ft(i,o),function(e,t){e.set("response_mode",t||x.QUERY)}(i,t.responseMode),Qt(i),function(e){e.set("clidata","1")}(i),t.prompt&&(!function(e,t){e.set("prompt",t)}(i,t.prompt),n?.addFields({prompt:t.prompt},o)),t.domainHint&&(!function(e,t){e.set("domain_hint",t)}(i,t.domainHint),n?.addFields({domainHintFromRequest:!0},o)),t.prompt!==R.SELECT_ACCOUNT)if(t.sid&&t.prompt===R.NONE)r.verbose("1tvqyx",t.correlationId),Ht(i,t.sid),n?.addFields({sidFromRequest:!0},o);else if(t.account){const e=(s=t.account,s.idTokenClaims?.sid||null);let a=function(e){return e.loginHint||e.idTokenClaims?.login_hint||null}(t.account);if(a&&t.domainHint&&(r.warning("0wkg3v",t.correlationId),a=null),a){r.verbose("1eyfsw",t.correlationId),Nt(i,a),n?.addFields({loginHintFromClaim:!0},o);try{Lt(i,Nr(t.account.homeAccountId))}catch(e){r.verbose("12ugck",t.correlationId)}}else if(e&&t.prompt===R.NONE){r.verbose("1rmd8s",t.correlationId),Ht(i,e),n?.addFields({sidFromClaim:!0},o);try{Lt(i,Nr(t.account.homeAccountId))}catch(e){r.verbose("12ugck",t.correlationId)}}else if(t.loginHint)r.verbose("0y3007",t.correlationId),Nt(i,t.loginHint),Ut(i,t.loginHint),n?.addFields({loginHintFromRequest:!0},o);else if(t.account.username){r.verbose("02f507",t.correlationId),Nt(i,t.account.username),n?.addFields({loginHintFromUpn:!0},o);try{Lt(i,Nr(t.account.homeAccountId))}catch(e){r.verbose("12ugck",t.correlationId)}}}else t.loginHint&&(r.verbose("0g01ey",t.correlationId),Nt(i,t.loginHint),Ut(i,t.loginHint),n?.addFields({loginHintFromRequest:!0},o));else r.verbose("169k9v",t.correlationId);var s;return t.nonce&&function(e,t){e.set("nonce",t)}(i,t.nonce),t.state&&zt(i,t.state),(t.claims||e.clientCapabilities&&e.clientCapabilities.length>0)&&Dt(i,t.claims,e.clientCapabilities),t.embeddedClientId&&nr(i,e.clientId,e.redirectUri),!e.instanceAware||t.extraQueryParameters&&Object.keys(t.extraQueryParameters).includes(fe)||Vt(i),i}function bo(e,t){const r=cr(t);return br.appendQueryString(e.authorizationEndpoint,r)}function Ao(e,t){if(!e.state||!t)throw e.state?be(rt,"Cached State"):be(rt,"Server State");let r,n;try{r=decodeURIComponent(e.state)}catch(t){throw be(et,e.state)}try{n=decodeURIComponent(t)}catch(t){throw be(et,e.state)}if(r!==n)throw be(tt);if(e.error||e.error_description||e.suberror){const t=function(e){const t="code=",r=e.error_uri?.lastIndexOf(t);return r&&r>=0?e.error_uri?.substring(r+t.length):void 0}(e);if(Vn(e.error,e.error_description,e.suberror))throw new Qn(e.error||"",e.error_description,e.suberror,e.timestamp||"",e.trace_id||"",e.correlation_id||"",e.claims||"",t);throw new Yn(e.error||"",e.error_description,e.suberror,t)}}function So(e,t){if(e){if(t.resource&&(_o(t.extraParameters)||_o(t.extraQueryParameters)))throw be(_t);if(!t.resource)throw be(St)}}function _o(e){return!!e&&Object.prototype.hasOwnProperty.call(e,"resource")}const Eo="unexpected_error";var Po=Object.freeze({__proto__:null,postRequestFailed:"post_request_failed",unexpectedError:Eo});function Ro(e){const{skus:t,libraryName:r,libraryVersion:n,extensionName:o,extensionVersion:i}=e,s=new Map([[0,[r,n]],[2,[o,i]]]);let a=[];if(t?.length){if(a=t.split(","),a.length<4)return t}else a=Array.from({length:4},(()=>"|"));return s.forEach(((e,t)=>{2===e.length&&e[0]?.length&&e[1]?.length&&function(e){const{skuArr:t,index:r,skuName:n,skuVersion:o}=e;if(r>=t.length)return;t[r]=[n,o].join("|")}({skuArr:a,index:t,skuName:e[0],skuVersion:e[1]})})),a.join(",")}class Oo{constructor(e,t){this.cacheOutcome=re,this.cacheManager=t,this.apiId=e.apiId,this.correlationId=e.correlationId,this.wrapperSKU=e.wrapperSKU||"",this.wrapperVer=e.wrapperVer||"",this.telemetryCacheKey=J+"-"+e.clientId}generateCurrentRequestHeaderValue(){const e=`${this.apiId}${W}${this.cacheOutcome}`,t=[this.wrapperSKU,this.wrapperVer],r=this.getNativeBrokerErrorCode();r?.length&&t.push(`broker_error=${r}`);const n=t.join(W),o=[e,this.getRegionDiscoveryFields()].join(W);return[$,o,n].join("|")}generateLastRequestHeaderValue(){const e=this.getLastRequests(),t=Oo.maxErrorsToSend(e),r=e.failedRequests.slice(0,2*t).join(W),n=e.errors.slice(0,t).join(W),o=e.errors.length,i=[o,t=50&&(t.failedRequests.shift(),t.failedRequests.shift(),t.errors.shift()),t.failedRequests.push(this.apiId,this.correlationId),e instanceof Error&&e&&e.toString()?e instanceof we?e.subError?t.errors.push(e.subError):e.errorCode?t.errors.push(e.errorCode):t.errors.push(e.toString()):t.errors.push(e.toString()):t.errors.push("unknown_error"),this.cacheManager.setServerTelemetry(this.telemetryCacheKey,t,this.correlationId)}incrementCacheHits(){const e=this.getLastRequests();return e.cacheHits+=1,this.cacheManager.setServerTelemetry(this.telemetryCacheKey,e,this.correlationId),e.cacheHits}getLastRequests(){return this.cacheManager.getServerTelemetry(this.telemetryCacheKey,this.correlationId)||{failedRequests:[],errors:[],cacheHits:0}}clearTelemetryCache(){const e=this.getLastRequests(),t=Oo.maxErrorsToSend(e);if(t===e.errors.length)this.cacheManager.removeItem(this.telemetryCacheKey,this.correlationId);else{const r={failedRequests:e.failedRequests.slice(2*t),errors:e.errors.slice(t),cacheHits:0};this.cacheManager.setServerTelemetry(this.telemetryCacheKey,r,this.correlationId)}}static maxErrorsToSend(e){let t,r=0,n=0;const o=e.errors.length;for(t=0;t")));for(let e=1;e=t);e++){const t=r[e];n.push(Uo(t))}return n}(e.stack,n)),r.errorName=e.name):t.trace("1cnpwa",r.correlationId));t.trace("0gcyox",r.correlationId)}function Uo(e){const t=e.lastIndexOf(" ")+1;if(t<1)return e;const r=e.substring(t);let n=r.lastIndexOf("/");return n=n<0?r.lastIndexOf("\\"):n,n>=0?(e.substring(0,t)+"("+r.substring(n+1)+(")"===r.charAt(r.length-1)?"":")")).trimStart():e.trimStart()}class Lo{constructor(e,t,r,n,o,i,s){this.authority=t,this.libraryName=n,this.libraryVersion=o,this.applicationTelemetry=i,this.clientId=e,this.logger=r,this.callbacks=new Map,this.eventsByCorrelationId=new Map,this.eventStack=new Map,this.intFields=s||new Set;for(const e of Vr)this.intFields.add(e)}startMeasurement(e,t){const r=t||this.generateId(),n={eventId:this.generateId(),status:Wr,authority:this.authority,libraryName:this.libraryName,libraryVersion:this.libraryVersion,clientId:this.clientId,name:e,startTimeMs:Date.now(),correlationId:r,appName:this.applicationTelemetry?.appName,appVersion:this.applicationTelemetry?.appVersion};var o,i;return this.cacheEventByCorrelationId(n),o=n,(i=this.eventStack.get(r))&&i.push({name:o.name}),{end:(e,t,r)=>this.endMeasurement({...n,...e},t,r),discard:()=>this.discardMeasurements(n.correlationId),add:e=>this.addFields(e,n.correlationId),increment:e=>this.incrementFields(e,n.correlationId),event:n}}endMeasurement(e,t,r){const n=this.eventsByCorrelationId.get(e.correlationId);if(!n)return this.logger.trace("0k9ti8",e.correlationId),null;const o=e.eventId===n.eventId;e.durationMs=Math.round(e.durationMs||this.getDurationMs(e.startTimeMs));const i=JSON.stringify(function(e,t,r){if(!t?.length)return;const n=e=>e.length?e[e.length-1]:void 0,o=e.name,i=n(t);if(i?.name!==o)return;const s=t?.pop();if(!s)return;const a=r instanceof we?r.errorCode:r instanceof Error?r.name:void 0,c=r instanceof we?r.subError:void 0;a&&s.childErr!==a&&(s.err=a,c&&(s.subErr=c)),delete s.name,delete s.childErr;const h={...s,dur:e.durationMs};e.success||(h.fail=1);const l=n(t);if(!l)return{[o]:h};let d;if(a&&(l.childErr=a),l[o]){const e=Object.keys(l).filter((e=>e.startsWith(o))).length;d=`${o}_${e+1}`}else d=o;return l[d]=h,l}(e,this.eventStack.get(n.correlationId),t));if(o?this.discardMeasurements(n.correlationId):n.incompleteSubMeasurements?.delete(e.eventId),t&&No(t,this.logger,n),!o)return n.ext={...n.ext,...e.ext},n.ext[e.name+"DurationMs"]=Math.floor(e.durationMs),{...n};o&&!t&&(n.errorCode||n.subErrorCode)&&(this.logger.trace("1fm1tm",e.correlationId),n.errorCode=void 0,n.subErrorCode=void 0);let s={...n,...e},a=0;s.incompleteSubMeasurements?.forEach((e=>{this.logger.trace("0nxk52",s.correlationId),a++})),s.incompleteSubMeasurements=void 0;const c=function(e){const t=[];for(const r of["",e]){const e=ur.get(r);t.push(...e?.logs??[]),ur.delete(r)}return t}(e.correlationId).map((e=>`${e.milliseconds},${e.hash}`)).join(";");return s={...s,status:Gr,incompleteSubsCount:a,context:i,logs:c},r&&(s.accountType=function(e){const t=e?.idTokenClaims;return t?.tfp||t?.acr?"B2C":t?.tid?"9188040d-6c67-4c5b-b112-36a304b66dad"===t?.tid?"MSA":"AAD":void 0}(r),s.dataBoundary=r.dataBoundary),this.truncateIntegralFields(s),this.emitEvents([s],e.correlationId),s}addFields(e,t){const r=this.eventsByCorrelationId.get(t);if(r){const n={},o={};for(const t in e)if(t.startsWith(Qr)){const r=t.slice(4),n=e[t];"string"!=typeof n&&"number"!=typeof n||(o[r]=n)}else n[t]=e[t];const i={...r,...n};Object.keys(o).length&&(i.ext={...i.ext,...o}),this.eventsByCorrelationId.set(t,i)}else this.logger.trace("0thl6s",t)}incrementFields(e,t){const r=this.eventsByCorrelationId.get(t);if(r)for(const t in e)if(t.startsWith(Qr)){r.ext=r.ext||{};const n=t.slice(4),o=r.ext[n];if(void 0===o)r.ext[n]=0;else if(isNaN(Number(o)))return;r.ext[n]=(Number(r.ext[n])||0)+(e[t]??0)}else{if(r.hasOwnProperty(t)){if(isNaN(Number(r[t])))return}else r[t]=0;r[t]+=e[t]}else this.logger.trace("0thl6s",t)}cacheEventByCorrelationId(e){const t=this.eventsByCorrelationId.get(e.correlationId);t?(t.incompleteSubMeasurements=t.incompleteSubMeasurements||new Map,t.incompleteSubMeasurements.set(e.eventId,{name:e.name,startTimeMs:e.startTimeMs})):(this.eventsByCorrelationId.set(e.correlationId,{...e}),this.eventStack.set(e.correlationId,[]))}discardMeasurements(e){this.eventsByCorrelationId.delete(e),this.eventStack.delete(e)}addPerformanceCallback(e){for(const[t,r]of this.callbacks)if(r.toString()===e.toString())return this.logger.warning("1eap5p",""),t;const t=this.generateId();return this.callbacks.set(t,e),this.logger.verbose("0c9ujz",""),t}removePerformanceCallback(e){const t=this.callbacks.delete(e);return t?this.logger.verbose("0253if",""):this.logger.verbose("0iqk07",""),t}emitEvents(e,t){this.logger.verbose("11jb1y",t),this.callbacks.forEach(((r,n)=>{this.logger.trace("0p2pjl",t),r.apply(null,[e])}))}truncateIntegralFields(e){this.intFields.forEach((t=>{t in e&&"number"==typeof e[t]&&(e[t]=Math.floor(e[t]))}))}getDurationMs(e){const t=Date.now()-e;return t<0?t:0}}const Ho="standardInteractionClientGetDiscoveredAuthority",Do="nativeInteractionClientAcquireToken",Fo="acquireTokenBySilentIframe",Ko="initializeBaseRequest",Bo="silentIframeClientTokenHelper",zo="silentHandlerInitiateAuthRequest",jo="silentHandlerMonitorIframeForHash",$o="standardInteractionClientCreateAuthCodeClient",Jo="standardInteractionClientGetClientConfiguration",Wo="standardInteractionClientInitializeAuthorizationRequest",Go="handleResponseEar",Qo="handleResponsePlatformBroker",Vo="handleResponseCode",Xo="deserializeResponse",Yo="removeHiddenIframe",Zo="generatePkceCodes",ei="generateHKDF",ti="decrypt",ri="generateEarKey",ni="pkce_not_created",oi="ear_jwk_empty",ii="ear_jwe_empty",si="crypto_nonexistent",ai="empty_navigate_uri",ci="hash_empty_error",hi="no_state_in_hash",li="hash_does_not_contain_known_properties",di="unable_to_parse_state",ui="state_interaction_type_mismatch",gi="interaction_in_progress",pi="interaction_in_progress_cancelled",mi="popup_window_error",fi="empty_window_error",yi="user_cancelled",wi="redirect_bridge_empty_response",Ii="redirect_in_iframe",Ci="block_iframe_reload",vi="block_nested_popups",ki="silent_logout_unsupported",Ti="no_account_error",bi="no_token_request_cache_error",Ai="unable_to_parse_token_request_cache_error",Si="non_browser_environment",_i="database_not_open",Ei="no_network_connectivity",Pi="post_request_failed",Ri="get_request_failed",Oi="failed_to_parse_response",Mi="unable_to_load_token",xi="crypto_key_not_found",qi="auth_code_required",Ni="auth_code_or_nativeAccountId_required",Ui="spa_code_and_nativeAccountId_present",Li="database_unavailable",Hi="unable_to_acquire_token_from_native_platform",Di="native_handshake_timeout",Fi="native_extension_not_installed",Ki="native_connection_not_established",Bi="uninitialized_public_client_application",zi="native_prompt_not_supported",ji="invalid_base64_string",$i="invalid_pop_token_request",Ji="failed_to_build_headers",Wi="failed_to_parse_headers",Gi="failed_to_decrypt_ear_response",Qi="timed_out",Vi="empty_response";var Xi=Object.freeze({__proto__:null,authCodeOrNativeAccountIdRequired:Ni,authCodeRequired:qi,authRequestNotSetError:"auth_request_not_set_error",blockIframeReload:Ci,blockNestedPopups:vi,cryptoKeyNotFound:xi,cryptoNonExistent:si,databaseNotOpen:_i,databaseUnavailable:Li,earJweEmpty:ii,earJwkEmpty:oi,emptyNavigateUri:ai,emptyResponse:Vi,emptyWindowError:fi,failedToBuildHeaders:Ji,failedToDecryptEarResponse:Gi,failedToParseHeaders:Wi,failedToParseResponse:Oi,getRequestFailed:Ri,hashDoesNotContainKnownProperties:li,hashEmptyError:ci,iframeClosedPrematurely:"iframe_closed_prematurely",interactionInProgress:gi,interactionInProgressCancelled:pi,invalidBase64String:ji,invalidCacheType:"invalid_cache_type",invalidPopTokenRequest:$i,nativeConnectionNotEstablished:Ki,nativeExtensionNotInstalled:Fi,nativeHandshakeTimeout:Di,nativePromptNotSupported:zi,noAccountError:Ti,noNetworkConnectivity:Ei,noStateInHash:hi,noTokenRequestCacheError:bi,nonBrowserEnvironment:Si,pkceNotCreated:ni,popupWindowError:mi,postRequestFailed:Pi,redirectBridgeEmptyResponse:wi,redirectInIframe:Ii,silentLogoutUnsupported:ki,silentPromptValueError:"silent_prompt_value_error",spaCodeAndNativeAccountIdPresent:Ui,stateInteractionTypeMismatch:ui,timedOut:Qi,unableToAcquireTokenFromNativePlatform:Hi,unableToLoadToken:Mi,unableToParseState:di,unableToParseTokenRequestCacheError:Ai,uninitializedPublicClientApplication:Bi,userCancelled:yi});function Yi(e){return`See https://aka.ms/msal.js.errors#${e} for details`}class Zi extends we{constructor(e,t){super(e,Yi(e),t),Object.setPrototypeOf(this,Zi.prototype),this.name="BrowserAuthError"}}function es(e,t){return new Zi(e,t)}const ts="invalid_grant",rs=483,ns=600,os="msal",is="msal.js.browser",ss="53ee284d-920a-4b59-9d30-a60315b26836",as="ppnbnpeolgkicgegkbkbjmhlideopiji",cs="MATS",hs="MicrosoftEntra",ls="DOM API",ds="get-token-and-sign-out",us="PlatformAuthDOMHandler",gs="PlatformAuthExtensionHandler",ps="Handshake",ms="HandshakeResponse",fs="GetToken",ys="Response",ws={LocalStorage:"localStorage",SessionStorage:"sessionStorage",MemoryStorage:"memoryStorage"},Is="GET",Cs="POST",vs="signin",ks="signout",Ts="request.origin",bs="urlHash",As="request.params",Ss="code.verifier",_s="interaction.status",Es="request.native",Ps="wrapper.sku",Rs="wrapper.version",Os={acquireTokenRedirect:861,acquireTokenPopup:862,ssoSilent:863,acquireTokenSilent_authCode:864,handleRedirectPromise:865,acquireTokenByCode:866,acquireTokenSilent_silentFlow:61,logout:961,logoutPopup:962,hydrateCache:963,loadExternalTokens:964},Ms={861:"acquireTokenRedirect",862:"acquireTokenPopup",863:"ssoSilent",864:"acquireTokenSilent_authCode",865:"handleRedirectPromise",866:"acquireTokenByCode",61:"acquireTokenSilent_silentFlow",961:"logout",962:"logoutPopup",963:"hydrateCache",964:"loadExternalTokens"};var xs;e.InteractionType=void 0,(xs=e.InteractionType||(e.InteractionType={})).Redirect="redirect",xs.Popup="popup",xs.Silent="silent",xs.None="none";const qs={Startup:"startup",Logout:"logout",AcquireToken:"acquireToken",HandleRedirect:"handleRedirect",None:"none"},Ns={scopes:p},Us="msal.db",Ls=`${Us}.keys`,Hs={Default:0,AccessToken:1,AccessTokenAndRefreshToken:2,RefreshToken:3,RefreshTokenAndNetwork:4,Skip:5},Ds=[Hs.Default,Hs.Skip,Hs.RefreshTokenAndNetwork];function Fs(e){return encodeURIComponent(Bs(e).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_"))}function Ks(e){return zs(e).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}function Bs(e){return zs((new TextEncoder).encode(e))}function zs(e){const t=Array.from(e,(e=>String.fromCodePoint(e))).join("");return btoa(t)}function js(e){return(new TextDecoder).decode($s(e))}function $s(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw es(ji)}const r=atob(t);return Uint8Array.from(r,(e=>e.codePointAt(0)||0))}const Js="AES-GCM",Ws="HKDF",Gs="SHA-256",Qs=new Uint8Array([1,0,1]),Vs="0123456789abcdef",Xs=new Uint32Array(1),Ys="raw",Zs="encrypt",ea="decrypt",ta={name:"RSASSA-PKCS1-v1_5",hash:Gs,modulusLength:2048,publicExponent:Qs};async function ra(e){const t=(new TextEncoder).encode(e);return window.crypto.subtle.digest(Gs,t)}function na(e){return window.crypto.getRandomValues(e)}function oa(){return window.crypto.getRandomValues(Xs),Xs[0]}function ia(){const e=Date.now(),t=1024*oa()+(1023&oa()),r=new Uint8Array(16),n=Math.trunc(t/2**30),o=t&2**30-1,i=oa();r[0]=e/2**40,r[1]=e/2**32,r[2]=e/2**24,r[3]=e/65536,r[4]=e/256,r[5]=e,r[6]=112|n>>>8,r[7]=n,r[8]=128|o>>>24,r[9]=o>>>16,r[10]=o>>>8,r[11]=o,r[12]=i>>>24,r[13]=i>>>16,r[14]=i>>>8,r[15]=i;let s="";for(let e=0;e>>4),s+=Vs.charAt(15&r[e]),3!==e&&5!==e&&7!==e&&9!==e||(s+="-");return s}async function sa(e){return window.crypto.subtle.exportKey("jwk",e)}async function aa(){const e=await ha(),t={alg:"dir",kty:"oct",k:Ks(new Uint8Array(e))};return Bs(JSON.stringify(t))}async function ca(e,t){const r=t.split(".");if(5!==r.length)throw es(Gi,"jwe_length");const n=await async function(e){const t=js(e),r=$s(JSON.parse(t).k);return window.crypto.subtle.importKey(Ys,r,Js,!1,[ea])}(e).catch((()=>{throw es(Gi,"import_key")}));try{const e=(new TextEncoder).encode(r[0]),t=$s(r[2]),o=$s(r[3]),i=$s(r[4]),s=8*i.byteLength,a=new Uint8Array(o.length+i.length);a.set(o),a.set(i,o.length);const c=await window.crypto.subtle.decrypt({name:Js,iv:t,tagLength:s,additionalData:e},n,a);return(new TextDecoder).decode(c)}catch(e){throw es(Gi,"decrypt")}}async function ha(){const e=await window.crypto.subtle.generateKey({name:Js,length:256},!0,[Zs,ea]);return window.crypto.subtle.exportKey(Ys,e)}async function la(e){return window.crypto.subtle.importKey(Ys,e,Ws,!1,["deriveKey"])}async function da(e,t,r){return window.crypto.subtle.deriveKey({name:Ws,salt:t,hash:Gs,info:(new TextEncoder).encode(r)},e,{name:Js,length:256},!1,[Zs,ea])}async function ua(e,t,r){const n=(new TextEncoder).encode(t),o=window.crypto.getRandomValues(new Uint8Array(16)),i=await da(e,o,r),s=await window.crypto.subtle.encrypt({name:Js,iv:new Uint8Array(12)},i,n);return{data:Ks(new Uint8Array(s)),nonce:Ks(o)}}async function ga(e,t,r,n){const o=$s(n),i=await da(e,$s(t),r),s=await window.crypto.subtle.decrypt({name:Js,iv:new Uint8Array(12)},i,o);return(new TextDecoder).decode(s)}const pa="storage_not_supported",ma="stubbed_public_client_application_called",fa="in_mem_redirect_unavailable";var ya=Object.freeze({__proto__:null,inMemRedirectUnavailable:fa,storageNotSupported:pa,stubbedPublicClientApplicationCalled:ma});class wa extends we{constructor(e,t){super(e,t),this.name="BrowserConfigurationAuthError",Object.setPrototypeOf(this,wa.prototype)}}function Ia(e){return new wa(e,Yi(e))}function Ca(){const e=window.location.hash,t=window.location.search;let r,n=!1,o=!1,i="";if(e&&e.length>1){const t="#"===e.charAt(0)?e.substring(1):e,o=new URLSearchParams(t);o.has("state")&&(n=!0,i=t,r=o)}if(t&&t.length>1){const e="?"===t.charAt(0)?t.substring(1):t,n=new URLSearchParams(e);n.has("state")&&(o=!0,i=e,r=n)}if(n&&o){i=`${"?"===t.charAt(0)?t.substring(1):t}${"#"===e.charAt(0)?e.substring(1):e}`,r=new URLSearchParams(i)}if(!i||!r)throw es(Vi);const s=r.get("state");if(!s)throw es(hi);const{libraryState:a}=eo(js,s),{id:c,meta:h}=a;if(!c||!h)throw es(di,"missing_library_state");return{params:r,payload:i,urlHash:e,urlQuery:t,hasResponseInHash:n,hasResponseInQuery:o,libraryState:{id:c,meta:h}}}function va(e){e.location.hash="","function"==typeof e.history.replaceState&&e.history.replaceState(null,"",`${e.location.origin}${e.location.pathname}${e.location.search}`)}function ka(e){const t=e.split("#");t.shift(),window.location.hash=t.length>0?t.join("#"):""}function Ta(){return window.parent!==window}function ba(){if(Ta())return!1;try{const{libraryState:t}=Ca(),{meta:r}=t;return r.interactionType===e.InteractionType.Popup}catch(e){return!1}}let Aa=null;function Sa(e,t){Aa&&(e.verbose("18y01k",t),clearTimeout(Aa.timeoutId),Aa.channel.close(),Aa.reject(es(pi)),Aa=null)}async function _a(e,t,r,n,o,i){return new Promise(((s,a)=>{t.verbose("1rf6em",n.correlationId);const c=n.correlationId;o.addFields({redirectBridgeTimeoutMs:e,lateResponseExperimentEnabled:i?.iframeTimeoutTelemetry||!1},c);const{libraryState:h}=eo(r.base64Decode,n.state||""),l=new BroadcastChannel(h.id);let d,u,g,p=!1;const m=window.setTimeout((()=>{Aa=null,i?.iframeTimeoutTelemetry?(g=o.startMeasurement("waitForBridgeLateResponse",c),p=!0,u=window.setTimeout((()=>{g?.end({success:!1}),clearTimeout(u),l.close()}),6e4)):l.close(),a(es(Qi,"redirect_bridge_timeout"))}),e);Aa={timeoutId:m,channel:l,reject:a},l.onmessage=e=>{d=e.data.payload;const t=e?.data&&"number"==typeof e.data.v?e.data.v:void 0;if(p)return g?.end({success:!!d}),clearTimeout(u),void l.close();o.addFields({redirectBridgeMessageVersion:t},c),Aa=null,clearTimeout(m),l.close(),d?s(d):a(es(wi))}}))}function Ea(){return"undefined"!=typeof window&&window.location?window.location.href.split("?")[0].split("#")[0]:""}function Pa(){const e=new br(window.location.href).getUrlComponents();return`${e.Protocol}//${e.HostNameAndPort}/`}function Ra(){if(ar(window.location.hash)&&Ta())throw es(Ci)}function Oa(e){if(Ta()&&!e)throw es(Ii)}function Ma(){if(ba())throw es(vi)}function xa(){if("undefined"==typeof window)throw es(Si)}function qa(e){if(!e)throw es(Bi)}function Na(e){xa(),Ra(),Ma(),qa(e)}function Ua(e,t){if(Na(e),Oa(t.system.allowRedirectInIframe),t.cache.cacheLocation===ws.MemoryStorage)throw Ia(fa)}function La(e){const t=document.createElement("link");t.rel="preconnect",t.href=new URL(e).origin,t.crossOrigin="anonymous",document.head.appendChild(t),window.setTimeout((()=>{try{document.head.removeChild(t)}catch{}}),1e4)}function Ha(){return ia()}const Da=Yt;var Fa=Object.freeze({__proto__:null,addClientCapabilitiesToClaims:Da,blockAPICallsBeforeInitialize:qa,blockAcquireTokenInPopups:Ma,blockNonBrowserEnvironment:xa,blockRedirectInIframe:Oa,blockReloadInHiddenIframes:Ra,cancelPendingBridgeResponse:Sa,clearHash:va,createGuid:Ha,getCurrentUri:Ea,getHomepage:Pa,invoke:xn,invokeAsync:qn,isInIframe:Ta,isInPopup:ba,parseAuthResponseFromUrl:Ca,preconnect:La,preflightCheck:Na,redirectPreflightCheck:Ua,replaceHash:ka,waitForBridgeResponse:_a});class Ka{constructor(){this.dbName=Us,this.version=1,this.tableName=Ls,this.dbOpen=!1}async open(){return new Promise(((e,t)=>{const r=window.indexedDB.open(this.dbName,this.version);r.addEventListener("upgradeneeded",(e=>{e.target.result.createObjectStore(this.tableName)})),r.addEventListener("success",(t=>{const r=t;this.db=r.target.result,this.dbOpen=!0,e()})),r.addEventListener("error",(()=>t(es(Li))))}))}closeConnection(){const e=this.db;e&&this.dbOpen&&(e.close(),this.dbOpen=!1)}async validateDbIsOpen(){if(!this.dbOpen)return this.open()}async getItem(e){return await this.validateDbIsOpen(),new Promise(((t,r)=>{if(!this.db)return r(es(_i));const n=this.db.transaction([this.tableName],"readonly").objectStore(this.tableName).get(e);n.addEventListener("success",(e=>{const r=e;this.closeConnection(),t(r.target.result)})),n.addEventListener("error",(e=>{this.closeConnection(),r(e)}))}))}async setItem(e,t){return await this.validateDbIsOpen(),new Promise(((r,n)=>{if(!this.db)return n(es(_i));const o=this.db.transaction([this.tableName],"readwrite").objectStore(this.tableName).put(t,e);o.addEventListener("success",(()=>{this.closeConnection(),r()})),o.addEventListener("error",(e=>{this.closeConnection(),n(e)}))}))}async removeItem(e){return await this.validateDbIsOpen(),new Promise(((t,r)=>{if(!this.db)return r(es(_i));const n=this.db.transaction([this.tableName],"readwrite").objectStore(this.tableName).delete(e);n.addEventListener("success",(()=>{this.closeConnection(),t()})),n.addEventListener("error",(e=>{this.closeConnection(),r(e)}))}))}async getKeys(){return await this.validateDbIsOpen(),new Promise(((e,t)=>{if(!this.db)return t(es(_i));const r=this.db.transaction([this.tableName],"readonly").objectStore(this.tableName).getAllKeys();r.addEventListener("success",(t=>{const r=t;this.closeConnection(),e(r.target.result)})),r.addEventListener("error",(e=>{this.closeConnection(),t(e)}))}))}async containsKey(e){return await this.validateDbIsOpen(),new Promise(((t,r)=>{if(!this.db)return r(es(_i));const n=this.db.transaction([this.tableName],"readonly").objectStore(this.tableName).count(e);n.addEventListener("success",(e=>{const r=e;this.closeConnection(),t(1===r.target.result)})),n.addEventListener("error",(e=>{this.closeConnection(),r(e)}))}))}async deleteDatabase(){return this.db&&this.dbOpen&&this.closeConnection(),new Promise(((e,t)=>{const r=window.indexedDB.deleteDatabase(Us),n=setTimeout((()=>t(!1)),200);r.addEventListener("success",(()=>(clearTimeout(n),e(!0)))),r.addEventListener("blocked",(()=>(clearTimeout(n),e(!0)))),r.addEventListener("error",(()=>(clearTimeout(n),t(!1))))}))}}class Ba{constructor(){this.cache=new Map}async initialize(){}getItem(e){return this.cache.get(e)||null}getUserData(e){return this.getItem(e)}setItem(e,t){this.cache.set(e,t)}async setUserData(e,t){this.setItem(e,t)}removeItem(e){this.cache.delete(e)}getKeys(){const e=[];return this.cache.forEach(((t,r)=>{e.push(r)})),e}containsKey(e){return this.cache.has(e)}clear(){this.cache.clear()}decryptData(){return Promise.resolve(null)}}class za{constructor(e){this.inMemoryCache=new Ba,this.indexedDBCache=new Ka,this.logger=e}handleDatabaseAccessError(e,t){if(!(e instanceof Zi&&e.errorCode===Li))throw e;this.logger.error("1wx7zz",t)}async getItem(e,t){const r=this.inMemoryCache.getItem(e);if(!r)try{return this.logger.verbose("0naxpl",t),await this.indexedDBCache.getItem(e)}catch(e){this.handleDatabaseAccessError(e,t)}return r}async setItem(e,t,r){this.inMemoryCache.setItem(e,t);try{await this.indexedDBCache.setItem(e,t)}catch(e){this.handleDatabaseAccessError(e,r)}}async removeItem(e,t){this.inMemoryCache.removeItem(e);try{await this.indexedDBCache.removeItem(e)}catch(e){this.handleDatabaseAccessError(e,t)}}async getKeys(e){const t=this.inMemoryCache.getKeys();if(0===t.length)try{return this.logger.verbose("1iqrbq",e),await this.indexedDBCache.getKeys()}catch(t){this.handleDatabaseAccessError(t,e)}return t}async containsKey(e,t){const r=this.inMemoryCache.containsKey(e);if(!r)try{return this.logger.verbose("03zl2j",t),await this.indexedDBCache.containsKey(e)}catch(e){this.handleDatabaseAccessError(e,t)}return r}clearInMemory(e){this.logger.verbose("03r21p",e),this.inMemoryCache.clear(),this.logger.verbose("0uksk1",e)}async clearPersistent(e){try{this.logger.verbose("0rdqut",e);const t=await this.indexedDBCache.deleteDatabase();return t&&this.logger.verbose("149ouc",e),t}catch(t){return this.handleDatabaseAccessError(t,e),!1}}}class ja{constructor(e,t,r){this.logger=e,function(e){if(!window)throw es(Si);if(!window.crypto)throw es(si);if(!e&&!window.crypto.subtle)throw es(si,"crypto_subtle_undefined")}(r??!1),this.cache=new za(this.logger),this.performanceClient=t}createNewGuid(){return ia()}base64Encode(e){return Bs(e)}base64Decode(e){return js(e)}base64UrlEncode(e){return Fs(e)}encodeKid(e){return this.base64UrlEncode(JSON.stringify({kid:e}))}async getPublicKeyThumbprint(e){const t=this.performanceClient?.startMeasurement("cryptoOptsGetPublicKeyThumbprint",e.correlationId),r=await async function(e,t){return window.crypto.subtle.generateKey(ta,e,t)}(ja.EXTRACTABLE,ja.POP_KEY_USAGES),n=await sa(r.publicKey),o=$a({e:n.e,kty:n.kty,n:n.n}),i=await this.hashString(o),s=await sa(r.privateKey),a=await async function(e,t,r){return window.crypto.subtle.importKey("jwk",e,ta,t,r)}(s,!1,["sign"]);return await this.cache.setItem(i,{privateKey:a,publicKey:r.publicKey,requestMethod:e.resourceRequestMethod,requestUri:e.resourceRequestUri},e.correlationId),t&&t.end({success:!0}),i}async removeTokenBindingKey(e,t){await this.cache.removeItem(e,t);if(await this.cache.containsKey(e,t))throw be(It)}async clearKeystore(e){this.cache.clearInMemory(e);try{return await this.cache.clearPersistent(e),!0}catch(t){return t instanceof Error?this.logger.error("1owpn8",e):this.logger.error("0yrmwo",e),!1}}async signJwt(e,t,r,n){const o=this.performanceClient?.startMeasurement("cryptoOptsSignJwt",n),i=await this.cache.getItem(t,n||"");if(!i)throw es(xi);const s=await sa(i.publicKey),a=$a(s),c=Fs(JSON.stringify({kid:t})),h=Fs(qo.getShrHeaderString({...r?.header,alg:s.alg,kid:c}));e.cnf={jwk:JSON.parse(a)};const l=`${h}.${Fs(JSON.stringify(e))}`,d=(new TextEncoder).encode(l),u=await async function(e,t){return window.crypto.subtle.sign(ta,e,t)}(i.privateKey,d),g=`${l}.${Ks(new Uint8Array(u))}`;return o&&o.end({success:!0}),g}async hashString(e){return async function(e){const t=await ra(e);return Ks(new Uint8Array(t))}(e)}}function $a(e){return JSON.stringify(e,Object.keys(e).sort())}ja.POP_KEY_USAGES=["sign","verify"],ja.EXTRACTABLE=!0;const Ja="acquireTokenSilent",Wa="acquireTokenByCode",Ga="acquireTokenPopup",Qa="acquireTokenPreRedirect",Va="acquireTokenRedirect",Xa="ssoSilent",Ya="initializeClientApplication",Za="localStorageUpdated",ec="loadExternalTokens";var tc=Object.freeze({__proto__:null,AcquireTokenByCode:Wa,AcquireTokenPopup:Ga,AcquireTokenPreRedirect:Qa,AcquireTokenRedirect:Va,AcquireTokenSilent:Ja,InitializeClientApplication:Ya,LoadExternalTokens:ec,LocalStorageUpdated:Za,SsoSilent:Xa});const rc="msal",nc="browser",oc=`${rc}.${nc}.log.level`,ic=`${rc}.${nc}.log.pii`,sc=`${rc}.${nc}.performance.enabled`,ac=`${rc}.${nc}.platform.auth.dom`,cc=`${rc}.version`,hc="account.keys",lc="token.keys";function dc(e=2){return e<1?`${rc}.${hc}`:`${rc}.${e}.${hc}`}function uc(e,t=2){return t<1?`${rc}.${lc}.${e}`:`${rc}.${t}.${lc}.${e}`}const gc=864e5,pc="Lax",mc="None";class fc{initialize(){return Promise.resolve()}getItem(e){const t=`${encodeURIComponent(e)}`,r=document.cookie.split(";");for(let e=0;e{const r=decodeURIComponent(e).trim().split("=");t.push(r[0])})),t}containsKey(e){return this.getKeys().includes(e)}decryptData(){return Promise.resolve(null)}}function yc(e,t){const r=e.getItem(dc(t));return r?JSON.parse(r):[]}function wc(e,t,r){const n=t.getItem(uc(e,r));if(n){const e=JSON.parse(n);if(e&&e.hasOwnProperty("idToken")&&e.hasOwnProperty("accessToken")&&e.hasOwnProperty("refreshToken"))return e}return{idToken:[],accessToken:[],refreshToken:[]}}function Ic(e){return e.hasOwnProperty("id")&&e.hasOwnProperty("nonce")&&e.hasOwnProperty("data")}const Cc="msal.cache.encryption";class vc{constructor(e,t,r){if(!window.localStorage)throw Ia(pa);this.memoryStorage=new Ba,this.initialized=!1,this.clientId=e,this.logger=t,this.performanceClient=r,this.broadcast=new BroadcastChannel("msal.broadcast.cache")}async initialize(e){const t=new fc,r=t.getItem(Cc);let n={key:"",id:""};if(r)try{n=JSON.parse(r)}catch(e){}if(n.key&&n.id){const t=xn($s,"base64Decode",this.logger,this.performanceClient,e)(n.key);this.encryptionCookie={id:n.id,key:await qn(la,ei,this.logger,this.performanceClient,e)(t)}}else{const r=ia(),n=await qn(ha,"generateBaseKey",this.logger,this.performanceClient,e)(),o=xn(Ks,"urlEncodeArr",this.logger,this.performanceClient,e)(new Uint8Array(n));this.encryptionCookie={id:r,key:await qn(la,ei,this.logger,this.performanceClient,e)(n)};const i={id:r,key:o};t.setItem(Cc,JSON.stringify(i),0,!0,mc)}await qn(this.importExistingCache.bind(this),"importExistingCache",this.logger,this.performanceClient,e)(e),this.broadcast.addEventListener("message",(t=>{this.updateCache(t,e)})),this.initialized=!0}getItem(e){return window.localStorage.getItem(e)}getUserData(e){if(!this.initialized)throw es(Bi);return this.memoryStorage.getItem(e)}async decryptData(e,t,r){if(!this.initialized||!this.encryptionCookie)throw es(Bi);if(t.id!==this.encryptionCookie.id)return this.performanceClient.incrementFields({encryptedCacheExpiredCount:1},r),null;const n=await qn(ga,ti,this.logger,this.performanceClient,r)(this.encryptionCookie.key,t.nonce,this.getContext(e),t.data);if(!n)return null;try{return{...JSON.parse(n),lastUpdatedAt:t.lastUpdatedAt}}catch(e){return this.performanceClient.incrementFields({encryptedCacheCorruptionCount:1},r),null}}setItem(e,t){window.localStorage.setItem(e,t)}async setUserData(e,t,r,n,o){if(!this.initialized||!this.encryptionCookie)throw es(Bi);if(o)this.setItem(e,t);else{const{data:o,nonce:i}=await qn(ua,"encrypt",this.logger,this.performanceClient,r)(this.encryptionCookie.key,t,this.getContext(e)),s={id:this.encryptionCookie.id,nonce:i,data:o,lastUpdatedAt:n};this.setItem(e,JSON.stringify(s))}this.memoryStorage.setItem(e,t),this.broadcast.postMessage({key:e,value:t,context:this.getContext(e)})}removeItem(e){this.memoryStorage.containsKey(e)&&(this.memoryStorage.removeItem(e),this.broadcast.postMessage({key:e,value:null,context:this.getContext(e)})),window.localStorage.removeItem(e)}getKeys(){return Object.keys(window.localStorage)}containsKey(e){return window.localStorage.hasOwnProperty(e)}clear(){this.memoryStorage.clear();yc(this).forEach((e=>this.removeItem(e)));const e=wc(this.clientId,this);e.idToken.forEach((e=>this.removeItem(e))),e.accessToken.forEach((e=>this.removeItem(e))),e.refreshToken.forEach((e=>this.removeItem(e))),this.getKeys().forEach((e=>{(e.startsWith(rc)||-1!==e.indexOf(this.clientId))&&this.removeItem(e)}))}async importExistingCache(e){if(!this.encryptionCookie)return;let t=yc(this);t=await this.importArray(t,e),t.length?this.setItem(dc(),JSON.stringify(t)):this.removeItem(dc());const r=wc(this.clientId,this);r.idToken=await this.importArray(r.idToken,e),r.accessToken=await this.importArray(r.accessToken,e),r.refreshToken=await this.importArray(r.refreshToken,e),r.idToken.length||r.accessToken.length||r.refreshToken.length?this.setItem(uc(this.clientId),JSON.stringify(r)):this.removeItem(uc(this.clientId))}async getItemFromEncryptedCache(e,t){if(!this.encryptionCookie)return null;const r=this.getItem(e);if(!r)return null;let n;try{n=JSON.parse(r)}catch(e){return null}return Ic(n)?n.id!==this.encryptionCookie.id?(this.performanceClient.incrementFields({encryptedCacheExpiredCount:1},t),null):(this.performanceClient.incrementFields({encryptedCacheCount:1},t),qn(ga,ti,this.logger,this.performanceClient,t)(this.encryptionCookie.key,n.nonce,this.getContext(e),n.data)):(this.performanceClient.incrementFields({unencryptedCacheCount:1},t),r)}async importArray(e,t){const r=[],n=[];return e.forEach((e=>{const o=this.getItemFromEncryptedCache(e,t).then((t=>{t?(this.memoryStorage.setItem(e,t),r.push(e)):this.removeItem(e)}));n.push(o)})),await Promise.all(n),r}getContext(e){let t="";return e.includes(this.clientId)&&(t=this.clientId),t}updateCache(e,t){this.logger.trace("17cxcm",t);const r=this.performanceClient.startMeasurement(Za);r.add({isBackground:!0});const{key:n,value:o,context:i}=e.data;return n?i&&i!==this.clientId?(this.logger.trace("04rtdy",t),void r.end({success:!1,errorCode:"contextMismatch"})):(o?(this.memoryStorage.setItem(n,o),this.logger.verbose("1vzsgt",t)):(this.memoryStorage.removeItem(n),this.logger.verbose("04ypih",t)),void r.end({success:!0})):(this.logger.error("0e10qr",t),void r.end({success:!1,errorCode:"noKey"}))}}class kc{constructor(){if(!window.sessionStorage)throw Ia(pa)}async initialize(){}getItem(e){return window.sessionStorage.getItem(e)}getUserData(e){return this.getItem(e)}setItem(e,t){window.sessionStorage.setItem(e,t)}async setUserData(e,t){this.setItem(e,t)}removeItem(e){window.sessionStorage.removeItem(e)}getKeys(){return Object.keys(window.sessionStorage)}containsKey(e){return window.sessionStorage.hasOwnProperty(e)}decryptData(){return Promise.resolve(null)}}const Tc={INITIALIZE_START:"msal:initializeStart",INITIALIZE_END:"msal:initializeEnd",ACTIVE_ACCOUNT_CHANGED:"msal:activeAccountChanged",LOGIN_SUCCESS:"msal:loginSuccess",ACQUIRE_TOKEN_START:"msal:acquireTokenStart",BROKERED_REQUEST_START:"msal:brokeredRequestStart",ACQUIRE_TOKEN_SUCCESS:"msal:acquireTokenSuccess",BROKERED_REQUEST_SUCCESS:"msal:brokeredRequestSuccess",ACQUIRE_TOKEN_FAILURE:"msal:acquireTokenFailure",BROKERED_REQUEST_FAILURE:"msal:brokeredRequestFailure",ACQUIRE_TOKEN_NETWORK_START:"msal:acquireTokenFromNetworkStart",HANDLE_REDIRECT_START:"msal:handleRedirectStart",HANDLE_REDIRECT_END:"msal:handleRedirectEnd",POPUP_OPENED:"msal:popupOpened",LOGOUT_START:"msal:logoutStart",LOGOUT_SUCCESS:"msal:logoutSuccess",LOGOUT_FAILURE:"msal:logoutFailure",LOGOUT_END:"msal:logoutEnd",RESTORE_FROM_BFCACHE:"msal:restoreFromBFCache",BROKER_CONNECTION_ESTABLISHED:"msal:brokerConnectionEstablished"},bc="@azure/msal-browser",Ac="5.6.3";function Sc(e,t){const r=e.indexOf(t);r>-1&&e.splice(r,1)}class _c extends $r{constructor(e,t,r,n,o,i,s){super(e,r,n,o,s),this.cacheConfig=t,this.logger=n,this.internalStorage=new Ba,this.browserStorage=Ec(e,t.cacheLocation,n,o),this.temporaryCacheStorage=Ec(e,ws.SessionStorage,n,o),this.cookieStorage=new fc,this.eventHandler=i}async initialize(e){this.performanceClient.addFields({cacheLocation:this.cacheConfig.cacheLocation,cacheRetentionDays:this.cacheConfig.cacheRetentionDays},e),await this.browserStorage.initialize(e),await this.migrateExistingCache(e),this.trackVersionChanges(e)}async migrateExistingCache(e){let t=yc(this.browserStorage),r=wc(this.clientId,this.browserStorage);this.performanceClient.addFields({preMigrateAcntCount:t.length,preMigrateATCount:r.accessToken.length,preMigrateITCount:r.idToken.length,preMigrateRTCount:r.refreshToken.length},e);for(let t=0;t<2;t++){const r=t;await this.removeStaleAccounts(t,r,e)}for(let t=0;t<2;t++){const r=t;await this.migrateIdTokens(t,r,e)}const n=this.getKMSIValues();for(let t=0;t<2;t++)await this.migrateAccessTokens(t,n,e),await this.migrateRefreshTokens(t,n,e);t=yc(this.browserStorage),r=wc(this.clientId,this.browserStorage),this.performanceClient.addFields({postMigrateAcntCount:t.length,postMigrateATCount:r.accessToken.length,postMigrateITCount:r.idToken.length,postMigrateRTCount:r.refreshToken.length},e)}async updateOldEntry(e,t){const r=this.browserStorage.getItem(e),n=this.validateAndParseJson(r||"");if(!n)return this.browserStorage.removeItem(e),null;if(n.lastUpdatedAt){if(gn(n.lastUpdatedAt,this.cacheConfig.cacheRetentionDays))return this.browserStorage.removeItem(e),this.performanceClient.incrementFields({expiredCacheRemovedCount:1},t),null}else n.lastUpdatedAt=Date.now().toString(),this.setItem(e,JSON.stringify(n),t);const o=Ic(n)?await this.browserStorage.decryptData(e,n,t):n;return o&&wn(o)?(In(o)||Cn(o))&&o.expiresOn&&un(o.expiresOn,300)?(this.browserStorage.removeItem(e),this.performanceClient.incrementFields({expiredCacheRemovedCount:1},t),null):o:(this.performanceClient.incrementFields({invalidCacheCount:1},t),null)}async removeStaleAccounts(e,t,r){const n=yc(this.browserStorage,e);if(0!==n.length){for(const e of[...n]){this.performanceClient.incrementFields({oldAcntCount:1},r);const o=this.browserStorage.getItem(e),i=this.validateAndParseJson(o||"");i?i.lastUpdatedAt?gn(i.lastUpdatedAt,this.cacheConfig.cacheRetentionDays)&&(await this.removeAccountOldSchema(e,i,t,r),Sc(n,e)):(i.lastUpdatedAt=Date.now().toString(),this.setItem(e,JSON.stringify(i),r)):Sc(n,e)}this.setAccountKeys(n,r,e)}}async removeAccountOldSchema(e,t,r,n){const o=Ic(t)?await this.browserStorage.decryptData(e,t,n):t,i=o?.homeAccountId;if(i){const e=this.getTokenKeys(r);[...e.idToken].filter((e=>e.includes(i))).forEach((t=>{this.browserStorage.removeItem(t),Sc(e.idToken,t)})),[...e.accessToken].filter((e=>e.includes(i))).forEach((t=>{this.browserStorage.removeItem(t),Sc(e.accessToken,t)})),[...e.refreshToken].filter((e=>e.includes(i))).forEach((t=>{this.browserStorage.removeItem(t),Sc(e.refreshToken,t)})),this.setTokenKeys(e,n,r)}this.performanceClient.incrementFields({expiredAcntRemovedCount:1},n),this.browserStorage.removeItem(e)}getKMSIValues(){const e={},t=this.getTokenKeys().idToken;for(const r of t){const t=this.browserStorage.getUserData(r);if(t){const r=JSON.parse(t),n=vr(r.secret,js);n&&(e[r.homeAccountId]=kr(n))}}return e}async migrateIdTokens(e,t,r){const n=wc(this.clientId,this.browserStorage,e);if(0===n.idToken.length)return;const o=wc(this.clientId,this.browserStorage,2),i=yc(this.browserStorage),s=yc(this.browserStorage,t);for(const e of[...n.idToken]){this.performanceClient.incrementFields({oldITCount:1},r);const t=await this.updateOldEntry(e,r);if(!t){Sc(n.idToken,e);continue}const a=i.find((e=>e.includes(t.homeAccountId))),c=s.find((e=>e.includes(t.homeAccountId)));let h=null;if(a)h=this.getAccount(a,r);else if(c){const e=this.browserStorage.getItem(c),t=this.validateAndParseJson(e||"");h=t&&Ic(t)?await this.browserStorage.decryptData(c,t,r):t}if(!h){this.performanceClient.incrementFields({skipITMigrateCount:1},r);continue}const l=vr(t.secret,js),d=this.generateCredentialKey(t),u=this.getIdTokenCredential(d,r),g=Object.keys(l).includes("signin_state"),p=u&&Object.keys(vr(u.secret,js)||{}).includes("signin_state");if(!u||t.lastUpdatedAt>u.lastUpdatedAt&&(g||!p)){const e=h.tenantProfiles||[],n=Fr(l)||h.realm;if(n&&!e.find((e=>e.tenantId===n))){const t=Ir(h.homeAccountId,h.localAccountId,n,l);e.push(t)}h.tenantProfiles=e;const s=this.generateAccountKey(Br(h)),a=kr(l);await this.setUserData(s,JSON.stringify(h),r,h.lastUpdatedAt,a),i.includes(s)||i.push(s),await this.setUserData(d,JSON.stringify(t),r,t.lastUpdatedAt,a),this.performanceClient.incrementFields({migratedITCount:1},r),o.idToken.push(d)}}this.setTokenKeys(n,r,e),this.setTokenKeys(o,r),this.setAccountKeys(i,r)}async migrateAccessTokens(e,t,r){const n=wc(this.clientId,this.browserStorage,e);if(0===n.accessToken.length)return;const o=wc(this.clientId,this.browserStorage,2);for(const e of[...n.accessToken]){this.performanceClient.incrementFields({oldATCount:1},r);const i=await this.updateOldEntry(e,r);if(!i){Sc(n.accessToken,e);continue}if(!(i.homeAccountId in t)){this.performanceClient.incrementFields({skipATMigrateCount:1},r);continue}const s=this.generateCredentialKey(i),a=t[i.homeAccountId];if(o.accessToken.includes(s)){const e=this.getAccessTokenCredential(s,r);(!e||i.lastUpdatedAt>e.lastUpdatedAt)&&(await this.setUserData(s,JSON.stringify(i),r,i.lastUpdatedAt,a),this.performanceClient.incrementFields({migratedATCount:1},r))}else await this.setUserData(s,JSON.stringify(i),r,i.lastUpdatedAt,a),this.performanceClient.incrementFields({migratedATCount:1},r),o.accessToken.push(s)}this.setTokenKeys(n,r,e),this.setTokenKeys(o,r)}async migrateRefreshTokens(e,t,r){const n=wc(this.clientId,this.browserStorage,e);if(0===n.refreshToken.length)return;const o=wc(this.clientId,this.browserStorage,2);for(const e of[...n.refreshToken]){this.performanceClient.incrementFields({oldRTCount:1},r);const i=await this.updateOldEntry(e,r);if(!i){Sc(n.refreshToken,e);continue}if(!(i.homeAccountId in t)){this.performanceClient.incrementFields({skipRTMigrateCount:1},r);continue}const s=this.generateCredentialKey(i),a=t[i.homeAccountId];if(o.refreshToken.includes(s)){const e=this.getRefreshTokenCredential(s,r);(!e||i.lastUpdatedAt>e.lastUpdatedAt)&&(await this.setUserData(s,JSON.stringify(i),r,i.lastUpdatedAt,a),this.performanceClient.incrementFields({migratedRTCount:1},r))}else await this.setUserData(s,JSON.stringify(i),r,i.lastUpdatedAt,a),this.performanceClient.incrementFields({migratedRTCount:1},r),o.refreshToken.push(s)}this.setTokenKeys(n,r,e),this.setTokenKeys(o,r)}trackVersionChanges(e){const t=this.browserStorage.getItem(cc);t&&(this.logger.info("1wuc87",e),this.performanceClient.addFields({previousLibraryVersion:t},e)),t!==Ac&&this.setItem(cc,Ac,e)}validateAndParseJson(e){if(!e)return null;try{const t=JSON.parse(e);return t&&"object"==typeof t?t:null}catch(e){return null}}setItem(e,t,r){const n=new Array(3).fill(0),o=[];for(let i=0;i<=20;i++)try{if(this.browserStorage.setItem(e,t),i>0)for(let e=0;e<=2;e++){const t=n.slice(0,e).reduce(((e,t)=>e+t),0);if(t>=i)break;const s=i>t+n[e]?t+n[e]:i;i>t&&n[e]>0&&this.removeAccessTokenKeys(o.slice(t,s),r,e)}break}catch(s){const a=xr(s);if(!(a.errorCode===Or&&i<20))throw a;if(!o.length)for(let r=0;r<=2;r++)if(e===uc(this.clientId,r)){const e=JSON.parse(t).accessToken;o.push(...e),n[r]=e.length}else{const e=this.getTokenKeys(r).accessToken;o.push(...e),n[r]=e.length}if(o.length<=i)throw a;this.removeAccessToken(o[i],r,!1)}}async setUserData(e,t,r,n,o){const i=new Array(3).fill(0),s=[];for(let a=0;a<=20;a++)try{if(await qn(this.browserStorage.setUserData.bind(this.browserStorage),"setUserData",this.logger,this.performanceClient,r)(e,t,r,n,o),a>0)for(let e=0;e<=2;e++){const t=i.slice(0,e).reduce(((e,t)=>e+t),0);if(t>=a)break;const n=a>t+i[e]?t+i[e]:a;a>t&&i[e]>0&&this.removeAccessTokenKeys(s.slice(t,n),r,e)}break}catch(e){const t=xr(e);if(!(t.errorCode===Or&&a<20))throw t;if(!s.length)for(let e=0;e<=2;e++){const t=this.getTokenKeys(e).accessToken;s.push(...t),i[e]=t.length}if(s.length<=a)throw t;this.removeAccessToken(s[a],r,!1)}}getAccount(e,t){this.logger.trace("1lfvm6",t);const r=this.browserStorage.getUserData(e);if(!r)return this.removeAccountKeyFromMap(e,t),null;const n=this.validateAndParseJson(r);if(!(n&&(o=n,o&&o.hasOwnProperty("homeAccountId")&&o.hasOwnProperty("environment")&&o.hasOwnProperty("realm")&&o.hasOwnProperty("localAccountId")&&o.hasOwnProperty("username")&&o.hasOwnProperty("authorityType"))))return null;var o;const i=$r.toObject({},n);var s;return this.performanceClient.addFields({accountCachedBy:(s=i.cachedByApiId,"number"==typeof s&&s in Ms?Ms[s]:"unknown")},t),i}async setAccount(e,t,r,n){this.logger.trace("1bz3wr",t);const o=this.generateAccountKey(Br(e)),i=Date.now().toString();e.lastUpdatedAt=i,e.cachedByApiId=n,await this.setUserData(o,JSON.stringify(e),t,i,r),this.addAccountKeyToMap(o,t),this.performanceClient.addFields({kmsi:r},t)}setAccountKeys(e,t,r=2){0===e.length?this.removeItem(dc(r)):this.setItem(dc(r),JSON.stringify(e),t)}getAccountKeys(){return yc(this.browserStorage)}addAccountKeyToMap(e,t){this.logger.trace("0rb85k",t),this.logger.tracePii("1l9bdo",t);const r=this.getAccountKeys();return-1===r.indexOf(e)?(r.push(e),this.setItem(dc(),JSON.stringify(r),t),this.logger.verbose("0xia39",t),!0):(this.logger.verbose("0161kk",t),!1)}removeAccountKeyFromMap(e,t){this.logger.trace("1jpigu",t),this.logger.tracePii("1xzspl",t);const r=this.getAccountKeys(),n=r.indexOf(e);n>-1?(r.splice(n,1),this.setAccountKeys(r,t)):this.logger.trace("1dytu2",t)}removeAccount(e,t){const r=this.getActiveAccount(t);r?.homeAccountId===e.homeAccountId&&r?.environment===e.environment&&this.setActiveAccount(null,t),super.removeAccount(e,t),this.removeAccountKeyFromMap(this.generateAccountKey(e),t),this.browserStorage.getKeys().forEach((t=>{t.includes(e.homeAccountId)&&t.includes(e.environment)&&this.browserStorage.removeItem(t)}))}removeIdToken(e,t){super.removeIdToken(e,t);const r=this.getTokenKeys(),n=r.idToken.indexOf(e);n>-1&&(this.logger.info("05udv9",t),r.idToken.splice(n,1),this.setTokenKeys(r,t))}removeAccessToken(e,t,r=!0){super.removeAccessToken(e,t),r&&this.removeAccessTokenKeys([e],t)}removeAccessTokenKeys(e,t,r=2){this.logger.trace("17o18n",t);const n=this.getTokenKeys(r);let o=0;if(e.forEach((e=>{const t=n.accessToken.indexOf(e);t>-1&&(n.accessToken.splice(t,1),o++)})),o>0)return this.logger.info("15i5d5",t),void this.setTokenKeys(n,t,r)}removeRefreshToken(e,t){super.removeRefreshToken(e,t);const r=this.getTokenKeys(),n=r.refreshToken.indexOf(e);n>-1&&(this.logger.info("1f4fq3",t),r.refreshToken.splice(n,1),this.setTokenKeys(r,t))}getTokenKeys(e=2){return wc(this.clientId,this.browserStorage,e)}setTokenKeys(e,t,r=2){0!==e.idToken.length||0!==e.accessToken.length||0!==e.refreshToken.length?this.setItem(uc(this.clientId,r),JSON.stringify(e),t):this.removeItem(uc(this.clientId,r))}getIdTokenCredential(e,t){const r=this.browserStorage.getUserData(e);if(!r)return this.logger.trace("1jukz6",t),this.removeIdToken(e,t),null;const n=this.validateAndParseJson(r);return n&&((o=n)&&wn(o)&&o.hasOwnProperty("realm")&&o.credentialType===L.ID_TOKEN)?(this.logger.trace("01ju66",t),n):(this.logger.trace("1jukz6",t),null);var o}async setIdTokenCredential(e,t,r){this.logger.trace("13hjll",t);const n=this.generateCredentialKey(e),o=Date.now().toString();e.lastUpdatedAt=o,await this.setUserData(n,JSON.stringify(e),t,o,r);const i=this.getTokenKeys();-1===i.idToken.indexOf(n)&&(this.logger.info("07jy92",t),i.idToken.push(n),this.setTokenKeys(i,t))}getAccessTokenCredential(e,t){const r=this.browserStorage.getUserData(e);if(!r)return this.logger.trace("0bqvx8",t),this.removeAccessTokenKeys([e],t),null;const n=this.validateAndParseJson(r);return n&&In(n)?(this.logger.trace("1o81rl",t),n):(this.logger.trace("0bqvx8",t),null)}async setAccessTokenCredential(e,t,r){this.logger.trace("1pondb",t);const n=this.generateCredentialKey(e),o=Date.now().toString();e.lastUpdatedAt=o,await this.setUserData(n,JSON.stringify(e),t,o,r);const i=this.getTokenKeys(),s=i.accessToken.indexOf(n);-1!==s&&i.accessToken.splice(s,1),this.logger.trace("1onhey",t),i.accessToken.push(n),this.setTokenKeys(i,t)}getRefreshTokenCredential(e,t){const r=this.browserStorage.getUserData(e);if(!r)return this.logger.trace("0jlizt",t),this.removeRefreshToken(e,t),null;const n=this.validateAndParseJson(r);return n&&Cn(n)?(this.logger.trace("0nokxi",t),n):(this.logger.trace("0jlizt",t),null)}async setRefreshTokenCredential(e,t,r){this.logger.trace("0tcg8d",t);const n=this.generateCredentialKey(e),o=Date.now().toString();e.lastUpdatedAt=o,await this.setUserData(n,JSON.stringify(e),t,o,r);const i=this.getTokenKeys();-1===i.refreshToken.indexOf(n)&&(this.logger.info("0eckjs",t),i.refreshToken.push(n),this.setTokenKeys(i,t))}getAppMetadata(e,t){const r=this.browserStorage.getItem(e);if(!r)return this.logger.trace("1q101h",t),null;const n=this.validateAndParseJson(r);return n&&(o=e,(i=n)&&0===o.indexOf(H)&&i.hasOwnProperty("clientId")&&i.hasOwnProperty("environment"))?(this.logger.trace("19pvg2",t),n):(this.logger.trace("1q101h",t),null);var o,i}setAppMetadata(e,t){this.logger.trace("0cyma6",t);const r=function({environment:e,clientId:t}){return[H,e,t].join("-").toLowerCase()}(e);this.setItem(r,JSON.stringify(e),t)}getServerTelemetry(e,t){const r=this.browserStorage.getItem(e);if(!r)return this.logger.trace("0jk19c",t),null;const n=this.validateAndParseJson(r);return n&&function(e,t){const r=0===e.indexOf(J);let n=!0;return t&&(n=t.hasOwnProperty("failedRequests")&&t.hasOwnProperty("errors")&&t.hasOwnProperty("cacheHits")),r&&n}(e,n)?(this.logger.trace("12jguk",t),n):(this.logger.trace("0jk19c",t),null)}setServerTelemetry(e,t,r){this.logger.trace("1poh61",r),this.setItem(e,JSON.stringify(t),r)}getAuthorityMetadata(e,t){const r=this.internalStorage.getItem(e);if(!r)return this.logger.trace("1r39oe",t),null;const n=this.validateAndParseJson(r);return n&&function(e,t){return!!t&&0===e.indexOf(F)&&t.hasOwnProperty("aliases")&&t.hasOwnProperty("preferred_cache")&&t.hasOwnProperty("preferred_network")&&t.hasOwnProperty("canonical_authority")&&t.hasOwnProperty("authorization_endpoint")&&t.hasOwnProperty("token_endpoint")&&t.hasOwnProperty("issuer")&&t.hasOwnProperty("aliasesFromNetwork")&&t.hasOwnProperty("endpointsFromNetwork")&&t.hasOwnProperty("expiresAt")&&t.hasOwnProperty("jwks_uri")}(e,n)?(this.logger.trace("1ohvk3",t),n):null}getAuthorityMetadataKeys(){return this.internalStorage.getKeys().filter((e=>this.isAuthorityMetadata(e)))}setWrapperMetadata(e,t){this.internalStorage.setItem(Ps,e),this.internalStorage.setItem(Rs,t)}getWrapperMetadata(){return[this.internalStorage.getItem(Ps)||"",this.internalStorage.getItem(Rs)||""]}setAuthorityMetadata(e,t,r){this.logger.trace("07w8n2",r),this.internalStorage.setItem(e,JSON.stringify(t))}getActiveAccount(e){const t=this.generateCacheKey(b),r=this.browserStorage.getItem(t);if(!r)return this.logger.trace("08gw0e",e),null;const n=this.validateAndParseJson(r);return n?(this.logger.trace("1t3ch7",e),this.getAccountInfoFilteredBy({homeAccountId:n.homeAccountId,localAccountId:n.localAccountId,tenantId:n.tenantId},e)):(this.logger.trace("0me1up",e),null)}setActiveAccount(e,t){const r=this.generateCacheKey(b);if(e){this.logger.verbose("0rsj80",t);const n={homeAccountId:e.homeAccountId,localAccountId:e.localAccountId,tenantId:e.tenantId};this.setItem(r,JSON.stringify(n),t)}else this.logger.verbose("1bp5z5",t),this.browserStorage.removeItem(r);this.eventHandler.emitEvent(Tc.ACTIVE_ACCOUNT_CHANGED,t)}getThrottlingCache(e,t){const r=this.browserStorage.getItem(e);if(!r)return this.logger.trace("1h4wa6",t),null;const n=this.validateAndParseJson(r);return n&&function(e,t){let r=!1;e&&(r=0===e.indexOf(Q));let n=!0;return t&&(n=t.hasOwnProperty("throttleTime")),r&&n}(e,n)?(this.logger.trace("0of6n8",t),n):(this.logger.trace("1h4wa6",t),null)}setThrottlingCache(e,t,r){this.logger.trace("0wfgh6",r),this.setItem(e,JSON.stringify(t),r)}getTemporaryCache(e,t,r){const n=r?this.generateCacheKey(e):e,o=this.temporaryCacheStorage.getItem(n);if(!o){if(this.cacheConfig.cacheLocation===ws.LocalStorage){const e=this.browserStorage.getItem(n);if(e)return this.logger.trace("1yt61y",t),e}return this.logger.trace("1qhy81",t),null}return o}setTemporaryCache(e,t,r){const n=r?this.generateCacheKey(e):e;this.temporaryCacheStorage.setItem(n,t)}removeItem(e){this.browserStorage.removeItem(e)}removeTemporaryItem(e){this.temporaryCacheStorage.removeItem(e)}getKeys(){return this.browserStorage.getKeys()}clear(e){this.removeAllAccounts(e),this.removeAppMetadata(e),this.temporaryCacheStorage.getKeys().forEach((e=>{-1===e.indexOf(rc)&&-1===e.indexOf(this.clientId)||this.removeTemporaryItem(e)})),this.browserStorage.getKeys().forEach((e=>{-1===e.indexOf(rc)&&-1===e.indexOf(this.clientId)||this.browserStorage.removeItem(e)})),this.internalStorage.clear()}generateCacheKey(e){return ke.startsWith(e,rc)?e:`${rc}.${this.clientId}.${e}`}generateCredentialKey(e){const t=e.credentialType===L.REFRESH_TOKEN&&e.familyId||e.clientId,r=e.tokenType&&e.tokenType.toLowerCase()!==G.BEARER.toLowerCase()?e.tokenType.toLowerCase():"";return[`${rc}.2`,e.homeAccountId,e.environment,e.credentialType,t,e.realm||"",e.target||"",r].join("|").toLowerCase()}generateAccountKey(e){const t=e.homeAccountId.split(".")[1];return[`${rc}.2`,e.homeAccountId,e.environment,t||e.tenantId||""].join("|").toLowerCase()}resetRequestCache(e){this.logger.trace("0h0ynu",e),this.removeTemporaryItem(this.generateCacheKey(As)),this.removeTemporaryItem(this.generateCacheKey(Ss)),this.removeTemporaryItem(this.generateCacheKey(Ts)),this.removeTemporaryItem(this.generateCacheKey(bs)),this.removeTemporaryItem(this.generateCacheKey(Es)),this.setInteractionInProgress(!1,void 0)}cacheAuthorizeRequest(e,t,r){this.logger.trace("1tzef5",t);const n=Bs(JSON.stringify(e));if(this.setTemporaryCache(As,n,!0),r){const e=Bs(r);this.setTemporaryCache(Ss,e,!0)}}getCachedRequest(e){this.logger.trace("0uen20",e);const t=this.getTemporaryCache(As,e,!0);if(!t)throw es(bi);const r=this.getTemporaryCache(Ss,e,!0);let n,o="";try{n=JSON.parse(js(t)),r&&(o=js(r))}catch(t){throw this.logger.errorPii("0ewsey",e),this.logger.error("0tvdic",e),es(Ai)}return[n,o]}getCachedNativeRequest(){this.logger.trace("1yxcdm","");const e=this.getTemporaryCache(Es,"",!0);if(!e)return this.logger.trace("0mnxd4",""),null;const t=this.validateAndParseJson(e);return t||(this.logger.error("0rrkip",""),null)}isInteractionInProgress(e){const t=this.getInteractionInProgress()?.clientId;return e?t===this.clientId:!!t}getInteractionInProgress(){const e=`${rc}.${_s}`,t=this.getTemporaryCache(e,"",!1);try{return t?JSON.parse(t):null}catch(t){return this.logger.error("0jjyys",""),this.removeTemporaryItem(e),this.resetRequestCache(""),va(window),null}}setInteractionInProgress(e,t=vs,r=!1,n=""){const o=`${rc}.${_s}`;if(e){if(this.getInteractionInProgress()){if(!r)throw es(gi);this.logger.warning("1pmscr",n),Sa(this.logger,n),this.removeTemporaryItem(o)}this.setTemporaryCache(o,JSON.stringify({clientId:this.clientId,type:t}),!1)}else e||this.getInteractionInProgress()?.clientId!==this.clientId||this.removeTemporaryItem(o)}async hydrateCache(e,t){const r=mn(e.account.homeAccountId,e.account.environment,e.idToken,this.clientId,e.tenantId),n=fn(e.account.homeAccountId,e.account.environment,e.accessToken,this.clientId,e.tenantId,e.scopes.join(" "),e.expiresOn?ln(e.expiresOn):0,e.extExpiresOn?ln(e.extExpiresOn):0,js,void 0,e.tokenType,void 0,t.sshKid);t.resource&&(n.resource=t.resource);const o={idToken:r,accessToken:n};return this.saveCacheRecord(o,e.correlationId,kr(vr(e.idToken,js)),Os.hydrateCache)}async saveCacheRecord(e,t,r,n,o){try{await super.saveCacheRecord(e,t,r,n,o)}catch(e){if(e instanceof Mr&&this.performanceClient&&t)try{const e=this.getTokenKeys();this.performanceClient.addFields({cacheRtCount:e.refreshToken.length,cacheIdCount:e.idToken.length,cacheAtCount:e.accessToken.length},t)}catch(e){}throw e}}}function Ec(e,t,r,n){try{switch(t){case ws.LocalStorage:return new vc(e,r,n);case ws.SessionStorage:return new kc}}catch(e){r.error(e,"")}return new Ba}const Pc=(e,t,r,n)=>new _c(e,{cacheLocation:ws.MemoryStorage,cacheRetentionDays:5},lr,t,r,n);function Rc(e,t,r,n,o){return e.verbose("1yd030",n),r?t.getAllAccounts(o,n):[]}function Oc(e,t,r,n){t.trace("0u7b90",n);const o=r.getAccountInfoFilteredBy(e,n);return o?(t.verbose("0btgll",n),o):(t.verbose("0ltaj5",n),null)}function Mc(e,t,r){t.setActiveAccount(e,r)}function xc(e,t){return e.getActiveAccount(t)}class qc{constructor(e){this.eventCallbacks=new Map,this.logger=e||new pr({}),"undefined"!=typeof BroadcastChannel&&(this.broadcastChannel=new BroadcastChannel("msal.broadcast.event")),this.invokeCrossTabCallbacks=this.invokeCrossTabCallbacks.bind(this)}addEventCallback(e,t,r){if("undefined"!=typeof window){const n=r||Ha();return this.eventCallbacks.has(n)?(this.logger.error("1578i0",""),null):(this.eventCallbacks.set(n,[e,t||[]]),this.logger.verbose("1cnec4",""),n)}return null}removeEventCallback(e){this.eventCallbacks.delete(e),this.logger.verbose("12zotd","")}emitEvent(e,t,r,n,o){const i={eventType:e,interactionType:r||null,payload:n||null,error:o||null,correlationId:t,timestamp:Date.now()};switch(e){case Tc.LOGIN_SUCCESS:case Tc.LOGOUT_SUCCESS:case Tc.ACTIVE_ACCOUNT_CHANGED:this.broadcastChannel?.postMessage(i)}this.invokeCallbacks(i)}invokeCallbacks(e){this.eventCallbacks.forEach((([t,r],n)=>{(0===r.length||r.includes(e.eventType))&&(this.logger.verbose("15jpwk",""),t.apply(null,[e]))}))}invokeCrossTabCallbacks(e){const t=e.data;this.invokeCallbacks(t)}subscribeCrossTab(){this.broadcastChannel?.addEventListener("message",this.invokeCrossTabCallbacks)}unsubscribeCrossTab(){this.broadcastChannel?.removeEventListener("message",this.invokeCrossTabCallbacks)}}class Nc{constructor(e,t,r,n,o,i,s,a,c){this.config=e,this.browserStorage=t,this.browserCrypto=r,this.networkClient=this.config.system.networkClient,this.eventHandler=o,this.navigationClient=i,this.platformAuthProvider=c,this.correlationId=a,this.logger=n.clone(is,Ac),this.performanceClient=s}}function Uc(e,t,r,n){r.verbose("0bd1la",n);const o=e||t||"";return br.getAbsoluteUrl(o,Ea())}function Lc(e,t,r,n,o,i){o.verbose("1p12tq",r);const s={clientId:t,correlationId:r,apiId:e,forceRefresh:!1,wrapperSKU:n.getWrapperMetadata()[0],wrapperVer:n.getWrapperMetadata()[1]};return new Oo(s,n)}async function Hc(e,t,r,n,o,i,s,a,c){const h=a&&a.hasOwnProperty("instance_aware")?a.instance_aware:void 0,l={protocolMode:e.system.protocolMode,OIDCOptions:e.auth.OIDCOptions,knownAuthorities:e.auth.knownAuthorities,cloudDiscoveryMetadata:e.auth.cloudDiscoveryMetadata,authorityMetadata:e.auth.authorityMetadata},d=i||e.auth.authority,u=h?.length?"true"===h:e.auth.instanceAware,g=c&&u?e.auth.authority.replace(br.getDomainFromUrl(d),c.environment):d,p=mo.generateAuthority(g,s||e.auth.azureCloudOptions),m=await qn(wo,"authorityFactoryCreateDiscoveredInstance",o,r,t)(p,e.system.networkClient,n,l,o,t,r);if(c&&!m.isAlias(c.environment))throw ve(ze);return m}async function Dc(e,t,r,n,o){if(o)try{e.removeAccount(o,n),r.verbose("0s4z6h",n)}catch(e){r.error("0mgg1d",n)}else try{r.verbose("0zj631",n),e.clear(n),await t.clearKeystore(n)}catch(e){r.error("12ih0c",n)}}async function Fc(e,t,r,n,o){const i=e.authority||t.auth.authority,s=[...e&&e.scopes||[]],a={...e,correlationId:e.correlationId,authority:i,scopes:s};if(a.authenticationScheme){if(a.authenticationScheme===G.SSH){if(!e.sshJwk)throw ve(Le);if(!e.sshKid)throw ve(He)}n.verbose("1ecmns",o)}else a.authenticationScheme=G.BEARER,n.verbose("1l4fwv",o);return a}async function Kc(e,t,r,n,o){const i=await qn(Fc,Ko,o,n,e.correlationId)(e,r,n,o,e.correlationId);return{...e,...i,account:t,forceRefresh:e.forceRefresh||!1}}function Bc(e,t){let r;const n=e.httpMethod;if(t===Kr.EAR){if(n&&n!==g)throw ve(je);r=g}else r=n||u;return r}class zc extends Nc{initializeLogoutRequest(e){this.logger.verbose("0546u4",this.correlationId);const t={correlationId:this.correlationId,...e};if(e)if(e.logoutHint)this.logger.verbose("12k4l4",this.correlationId);else if(e.account){const r=this.getLogoutHintFromIdTokenClaims(e.account);r&&(this.logger.verbose("0st5di",this.correlationId),t.logoutHint=r)}else this.logger.verbose("0pdtc3",this.correlationId);else this.logger.verbose("07ndze",this.correlationId);return e&&null===e.postLogoutRedirectUri?this.logger.verbose("0ljv63",t.correlationId):e&&e.postLogoutRedirectUri?(this.logger.verbose("1vamm6",t.correlationId),t.postLogoutRedirectUri=br.getAbsoluteUrl(e.postLogoutRedirectUri,Ea())):null===this.config.auth.postLogoutRedirectUri?this.logger.verbose("15m5g7",t.correlationId):this.config.auth.postLogoutRedirectUri?(this.logger.verbose("1f4xlz",t.correlationId),t.postLogoutRedirectUri=br.getAbsoluteUrl(this.config.auth.postLogoutRedirectUri,Ea())):(this.logger.verbose("17s5rf",t.correlationId),t.postLogoutRedirectUri=br.getAbsoluteUrl(Ea(),Ea())),t}getLogoutHintFromIdTokenClaims(e){const t=e.idTokenClaims;if(t){if(t.login_hint)return t.login_hint;this.logger.verbose("0mvp54",this.correlationId)}else this.logger.verbose("1e7bdp",this.correlationId);return null}async createAuthCodeClient(e){const t=await qn(this.getClientConfiguration.bind(this),Jo,this.logger,this.performanceClient,this.correlationId)(e);return new Io(t,this.performanceClient)}async getClientConfiguration(e){const{serverTelemetryManager:t,requestAuthority:r,requestAzureCloudOptions:n,requestExtraQueryParameters:o,account:i}=e,s=e.authority||await qn(Hc,Ho,this.logger,this.performanceClient,this.correlationId)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,r,n,o,i),a=this.config.system.loggerOptions;return{authOptions:{clientId:this.config.auth.clientId,authority:s,clientCapabilities:this.config.auth.clientCapabilities,redirectUri:this.config.auth.redirectUri,isMcp:this.config.auth.isMcp},systemOptions:{tokenRenewalOffsetSeconds:this.config.system.tokenRenewalOffsetSeconds,preventCorsPreflight:!0},loggerOptions:{loggerCallback:a.loggerCallback,piiLoggingEnabled:a.piiLoggingEnabled,logLevel:a.logLevel,correlationId:this.correlationId},cryptoInterface:this.browserCrypto,networkInterface:this.networkClient,storageInterface:this.browserStorage,serverTelemetryManager:t,libraryInfo:{sku:is,version:Ac,cpu:"",os:""},telemetry:this.config.telemetry}}}async function jc(e,t,r,n,o,i,s,a){const c=Uc(e.redirectUri,r.auth.redirectUri,i,a);new URL(c).origin!==new URL(window.location.href).origin&&(i.warning("08qbvw",a),s.addFields({isRedirectUriCrossOrigin:!0},a));const h={interactionType:t},l=Zn(n,e&&e.state||"",h),d={...await qn(Fc,Ko,i,s,a)({...e,correlationId:a},r,s,i,a),redirectUri:c,state:l,nonce:e.nonce||ia(),responseMode:r.auth.OIDCOptions.responseMode},u={...d,httpMethod:Bc(d,r.system.protocolMode)};if(e.loginHint||e.sid)return u;const g=e.account||o.getActiveAccount(a);return g&&(i.verbose("1eqlb3",a),i.verbosePii("0tf99t",a),u.account=g),u}function $c(e,t,r,n){const o=ar(e);if(!o)throw sr(e)?(r.error("13pl0s",n),r.errorPii("1097vx",n),es(li)):(r.error("18h0l1",n),es(ci));return o}function Jc(e,t,r){if(!e.state)throw es(hi);const n=function(e,t){if(!t)return null;try{return eo(e.base64Decode,t).libraryState.meta}catch(e){throw be(et)}}(t,e.state);if(!n)throw es(di);if(n.interactionType!==r)throw es(ui)}class Wc{constructor(e,t,r,n,o){this.authModule=e,this.browserStorage=t,this.authCodeRequest=r,this.logger=n,this.performanceClient=o}async handleCodeResponse(e,t,r){let n;try{n=function(e,t){if(Ao(e,t),!e.code)throw be(wt);return e}(e,t.state)}catch(e){throw e instanceof Yn&&e.subError===yi?es(yi):e}return qn(this.handleCodeResponseFromServer.bind(this),En,this.logger,this.performanceClient,t.correlationId)(n,t,r)}async handleCodeResponseFromServer(e,t,r,n=!0){if(this.logger.trace("0mf2hb",t.correlationId),this.authCodeRequest.code=e.code,n&&(e.nonce=t.nonce||void 0),e.state=t.state,e.client_info)this.authCodeRequest.clientInfo=e.client_info;else{const e=this.createCcsCredentials(t);e&&(this.authCodeRequest.ccsCredential=e)}return await qn(this.authModule.acquireToken.bind(this.authModule),"authClientAcquireToken",this.logger,this.performanceClient,t.correlationId)(this.authCodeRequest,r,e)}createCcsCredentials(e){return e.account?{credential:e.account.homeAccountId,type:no}:e.loginHint?{credential:e.loginHint,type:oo}:null}}class Gc extends we{constructor(e,t,r){super(e,t||Yi(e)),Object.setPrototypeOf(this,Gc.prototype),this.name="NativeAuthError",this.ext=r}}function Qc(e){if(e.ext&&e.ext.status&&("PERSISTENT_ERROR"===e.ext.status||"DISABLED"===e.ext.status))return!0;if(e.ext&&e.ext.error&&-2147186943===e.ext.error)return!0;switch(e.errorCode){case"ContentError":case"PageException":return!0;default:return!1}}function Vc(e,t,r){if(r&&r.status)switch(r.status){case"ACCOUNT_UNAVAILABLE":return Xn(Hn,Yi(e));case"USER_INTERACTION_REQUIRED":return new Qn(e,t);case"USER_CANCEL":return es(yi);case"NO_NETWORK":return es(Ei);case"UX_NOT_ALLOWED":return Xn(Fn)}return new Gc(e,t,r)}class Xc extends zc{async acquireToken(e){const t=Lc(Os.acquireTokenSilent_silentFlow,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger),r=await qn(this.getClientConfiguration.bind(this),Jo,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:t,requestAuthority:e.authority,requestAzureCloudOptions:e.azureCloudOptions,account:e.account}),n=new vo(r,this.performanceClient);this.logger.verbose("0wa871",this.correlationId);try{const t=(await qn(n.acquireCachedToken.bind(n),"silentFlowClientAcquireCachedToken",this.logger,this.performanceClient,e.correlationId)(e))[0];return this.performanceClient.addFields({fromCache:!0},e.correlationId),t}catch(e){throw e instanceof Zi&&e.errorCode===xi&&this.logger.verbose("06wena",this.correlationId),e}}logout(e){this.logger.verbose("1rkurh",this.correlationId);const t=this.initializeLogoutRequest(e);return Dc(this.browserStorage,this.browserCrypto,this.logger,this.correlationId,t.account)}}class Yc extends Nc{constructor(e,t,r,n,o,i,s,a,c,h,l,d){super(e,t,r,n,o,i,a,d,c),this.apiId=s,this.accountId=h,this.platformAuthProvider=c,this.nativeStorageManager=l,this.silentCacheClient=new Xc(e,this.nativeStorageManager,r,n,o,i,a,d,c);const u=this.platformAuthProvider.getExtensionName();this.skus=Oo.makeExtraSkuString({libraryName:is,libraryVersion:Ac,extensionName:u,extensionVersion:this.platformAuthProvider.getExtensionVersion()})}addRequestSKUs(e){e.extraParameters={...e.extraParameters,[ge]:this.skus}}async acquireToken(e,t){this.logger.trace("03qeos",this.correlationId);const r=this.performanceClient.startMeasurement(Do,e.correlationId),n=hn(),o=Lc(this.apiId,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger);try{const i=await this.initializeNativeRequest(e);try{const e=await this.acquireTokensFromCache(this.accountId,i);return r.end({success:!0,isNativeBroker:!1,fromCache:!0}),e}catch(e){if(t===Hs.AccessToken)throw this.logger.info("0eitbc",this.correlationId),e;this.logger.info("0957j1",this.correlationId)}const s=await this.platformAuthProvider.sendMessage(i);return await this.handleNativeResponse(s,i,n).then((e=>(r.end({success:!0,isNativeBroker:!0,requestId:e.requestId}),o.clearNativeBrokerErrorCode(),e))).catch((e=>{throw r.end({success:!1,errorCode:e.errorCode,subErrorCode:e.subError,isNativeBroker:!0}),e}))}catch(e){throw e instanceof Gc&&o.setNativeBrokerErrorCode(e.errorCode),e}}createSilentCacheRequest(e,t){return{authority:e.authority,correlationId:this.correlationId,scopes:Pt.fromString(e.scope).asArray(),account:t,forceRefresh:!1}}async acquireTokensFromCache(e,t){if(!e)throw this.logger.warning("1ndf3e",this.correlationId),be(pt);const r=this.browserStorage.getBaseAccountInfo({nativeAccountId:e},t.correlationId);if(!r)throw be(pt);try{const e=this.createSilentCacheRequest(t,r),n=await this.silentCacheClient.acquireToken(e),o={...r,idTokenClaims:n?.idTokenClaims,idToken:n?.idToken};return{...n,account:o}}catch(e){throw e}}async acquireTokenRedirect(e,t,r){this.logger.trace("0luikq",this.correlationId);const n=await this.initializeNativeRequest(e),o=r?.navigateToLoginRequestUrl??!0;try{await this.platformAuthProvider.sendMessage(n)}catch(e){if(e instanceof Gc){if(Lc(this.apiId,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger).setNativeBrokerErrorCode(e.errorCode),Qc(e))throw e}}this.browserStorage.setTemporaryCache(Es,JSON.stringify(n),!0);const i={apiId:Os.acquireTokenRedirect,timeout:this.config.system.redirectNavigationTimeout,noHistory:!1},s=o?window.location.href:Uc(e.redirectUri,this.config.auth.redirectUri,this.logger,this.correlationId);t.end({success:!0}),await this.navigationClient.navigateExternal(s,i)}async handleRedirectPromise(e,t){if(this.logger.trace("1c5lhw",this.correlationId),!this.browserStorage.isInteractionInProgress(!0))return this.logger.info("0le6uv",this.correlationId),null;const r=this.browserStorage.getCachedNativeRequest();if(!r)return this.logger.verbose("0a6zjb",this.correlationId),e&&t&&e?.addFields({errorCode:"no_cached_request"},t),null;const{prompt:n,...o}=r;n&&this.logger.verbose("0ac34v",this.correlationId),this.browserStorage.removeItem(this.browserStorage.generateCacheKey(Es));const i=hn();try{this.logger.verbose("003x5a",this.correlationId);const e=await this.platformAuthProvider.sendMessage(o),t=await this.handleNativeResponse(e,o,i);return Lc(this.apiId,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger).clearNativeBrokerErrorCode(),t}catch(e){throw e}}logout(){return this.logger.trace("0u2sjm",this.correlationId),Promise.reject("Logout not implemented yet")}async handleNativeResponse(e,t,r){this.logger.trace("1bojln",this.correlationId);const n=vr(e.id_token,js),o=this.createHomeAccountIdentifier(e,n),i=this.browserStorage.getAccountInfoFilteredBy({nativeAccountId:t.accountId},this.correlationId)?.homeAccountId;if(t.extraParameters?.child_client_id&&e.account.id!==t.accountId)this.logger.info("1ub1in",this.correlationId);else if(o!==i&&e.account.id!==t.accountId)throw Vc("user_switch");const s=await Hc(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,t.authority),a=ro(this.browserStorage,s,o,js,this.correlationId,n,e.client_info,s.getPreferredCache(),n.tid,void 0,e.account.id,this.logger,this.performanceClient);e.expires_in=Number(e.expires_in);const c=await this.generateAuthenticationResult(e,t,n,a,s.canonicalAuthority,r);return await this.cacheAccount(a,kr(n)),await this.cacheNativeTokens(e,t,o,n,e.access_token,c.tenantId,r),c}createHomeAccountIdentifier(e,t){return jr(e.client_info||"",Ur,this.logger,this.browserCrypto,this.correlationId,t)}generateScopes(e,t){return t?Pt.fromString(t):Pt.fromString(e)}async generatePopAccessToken(e,t){if(t.tokenType===G.POP&&t.signPopToken){if(e.shr)return this.logger.trace("0coqhu",this.correlationId),e.shr;const r=new Un(this.browserCrypto,this.performanceClient),n={resourceRequestMethod:t.resourceRequestMethod,resourceRequestUri:t.resourceRequestUri,shrClaims:t.shrClaims,shrNonce:t.shrNonce,correlationId:this.correlationId};if(!t.keyId)throw be(vt);return r.signPopToken(e.access_token,t.keyId,n)}return e.access_token}async generateAuthenticationResult(e,t,r,n,o,i){const s=this.addTelemetryFromNativeResponse(e.properties.MATS),a=this.generateScopes(t.scope,e.scope),c=e.account.properties||{},h=c.UID||r.oid||r.sub||"",l=c.TenantId||r.tid||"",d=Cr(Br(n),void 0,r,e.id_token);d.nativeAccountId!==e.account.id&&(d.nativeAccountId=e.account.id);const u=await this.generatePopAccessToken(e,t),g=t.tokenType===G.POP?G.POP:G.BEARER;return{authority:o,uniqueId:h,tenantId:l,scopes:a.asArray(),account:d,idToken:e.id_token,idTokenClaims:r,accessToken:u,fromCache:!!s&&this.isResponseFromCache(s),expiresOn:dn(i+e.expires_in),tokenType:g,correlationId:this.correlationId,state:e.state,fromPlatformBroker:!0,...t.resource&&{resource:t.resource}}}async cacheAccount(e,t){await this.browserStorage.setAccount(e,this.correlationId,t,this.apiId),this.browserStorage.removeAccountContext(Br(e),this.correlationId)}cacheNativeTokens(e,t,r,n,o,i,s){const a=mn(r,t.authority,e.id_token||"",t.clientId,n.tid||""),c=s+(t.tokenType===G.POP?240:("string"==typeof e.expires_in?parseInt(e.expires_in,10):e.expires_in)||0),h=this.generateScopes(e.scope,t.scope),l={idToken:a,accessToken:fn(r,t.authority,o,t.clientId,n.tid||i,h.printScopes(),c,0,js,void 0,t.tokenType,void 0,t.keyId)};return this.nativeStorageManager.saveCacheRecord(l,this.correlationId,kr(n),this.apiId,t.storeInCache)}getExpiresInValue(e,t){return e===G.POP?240:("string"==typeof t?parseInt(t,10):t)||0}addTelemetryFromNativeResponse(e){const t=this.getMATSFromResponse(e);return t?(this.performanceClient.addFields({extensionId:this.platformAuthProvider.getExtensionId(),extensionVersion:this.platformAuthProvider.getExtensionVersion(),matsBrokerVersion:t.broker_version,matsAccountJoinOnStart:t.account_join_on_start,matsAccountJoinOnEnd:t.account_join_on_end,matsDeviceJoin:t.device_join,matsPromptBehavior:t.prompt_behavior,matsApiErrorCode:t.api_error_code,matsUiVisible:t.ui_visible,matsSilentCode:t.silent_code,matsSilentBiSubCode:t.silent_bi_sub_code,matsSilentMessage:t.silent_message,matsSilentStatus:t.silent_status,matsHttpStatus:t.http_status,matsHttpEventCount:t.http_event_count},this.correlationId),t):null}getMATSFromResponse(e){if(e)try{return JSON.parse(e)}catch(e){this.logger.error("0b3l57",this.correlationId)}return null}isResponseFromCache(e){return void 0===e.is_cached?(this.logger.verbose("1okqev",this.correlationId),!1):!!e.is_cached}async initializeNativeRequest(e){this.logger.trace("04j6wj",this.correlationId);const t=await this.getCanonicalAuthority(e),{scopes:r,...n}=e,o=new Pt(r||[]);o.appendScopes(p);const i={...n,accountId:this.accountId,clientId:this.config.auth.clientId,authority:t.urlString,scope:o.printScopes(),redirectUri:Uc(e.redirectUri,this.config.auth.redirectUri,this.logger,this.correlationId),prompt:this.getPrompt(e.prompt),correlationId:this.correlationId,tokenType:e.authenticationScheme,windowTitleSubstring:document.title,extraParameters:{...e.extraParameters},extendedExpiryToken:!1,keyId:e.popKid};if(i.signPopToken&&e.popKid)throw es($i);if(this.handleExtraBrokerParams(i),i.extraParameters=i.extraParameters||{},i.extraParameters.telemetry=cs,e.authenticationScheme===G.POP){const t={resourceRequestUri:e.resourceRequestUri,resourceRequestMethod:e.resourceRequestMethod,shrClaims:e.shrClaims,shrNonce:e.shrNonce,correlationId:this.correlationId},r=new Un(this.browserCrypto,this.performanceClient);let n;if(i.keyId)n=this.browserCrypto.base64UrlEncode(JSON.stringify({kid:i.keyId})),i.signPopToken=!1;else{const e=await qn(r.generateCnf.bind(r),Pn,this.logger,this.performanceClient,this.correlationId)(t,this.logger);n=e.reqCnfString,i.keyId=e.kid,i.signPopToken=!0}i.reqCnf=n}return this.addRequestSKUs(i),i}async getCanonicalAuthority(e){const t=e.authority||this.config.auth.authority,{azureCloudOptions:r,account:n}=e;n&&await Hc(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,t,r,void 0,n);const o=new br(t);return o.validateAsUri(),o}getPrompt(e){switch(this.apiId){case Os.ssoSilent:case Os.acquireTokenSilent_silentFlow:return this.logger.trace("1hiwaz",this.correlationId),R.NONE}if(e)switch(e){case R.NONE:case R.CONSENT:case R.LOGIN:return this.logger.trace("1ynje4",this.correlationId),e;default:throw this.logger.trace("0nkr6q",this.correlationId),es(zi)}else this.logger.trace("1qlu04",this.correlationId)}handleExtraBrokerParams(e){const t=e.extraParameters&&e.extraParameters.hasOwnProperty(pe)&&e.extraParameters.hasOwnProperty(me)&&e.extraParameters.hasOwnProperty(ce);if(!e.embeddedClientId&&!t)return;let r="";const n=e.redirectUri;e.embeddedClientId?(e.redirectUri=this.config.auth.redirectUri,r=e.embeddedClientId):e.extraParameters&&(e.redirectUri=e.extraParameters[me],r=e.extraParameters[ce]),e.extraParameters={child_client_id:r,child_redirect_uri:n},this.performanceClient?.addFields({embeddedClientId:r,embeddedRedirectUri:n},e.correlationId)}}const Zc=new Map([["e","AAD"],["m","MSA"]]);function eh(e,t,r){const n=function(e){if(!e)return null;try{const t=(/%(?:[0-9A-Fa-f]{2})/.test(e)?decodeURIComponent(e):e).split("|");return t.length<5?null:{accountType:Zc.get(t[0]?.trim()||"")||"",error:t[1]?.trim()||"",subError:t[2]?.trim()||"",cloudInstance:t[3]?.trim()||"",callerDataBoundary:t[4]?.trim()||""}}catch{return null}}(e.clientdata);n?.accountType&&r.addFields({accountType:n.accountType},t),n?.error&&r.addFields({serverErrorNo:n.error},t),n?.subError&&r.addFields({serverSubErrorNo:n.subError},t)}async function th(e,t,r,n,o){const i=To({...e.auth,authority:t},r,n,o);if(Kt(i,{sku:is,version:Ac,os:"",cpu:""}),e.system.protocolMode!==Kr.OIDC&&Bt(i,e.telemetry.application),r.platformBroker&&(function(e){e.set("nativebroker","1")}(i),r.authenticationScheme===G.POP)){const e=new ja(n,o),t=new Un(e,o);let s;if(r.popKid)s=e.encodeKid(r.popKid);else{s=(await qn(t.generateCnf.bind(t),Pn,n,o,r.correlationId)(r,n)).reqCnfString}Zt(i,s)}return Rt(i,r.correlationId,o),i}async function rh(e,t,r,n,o){if(!r.codeChallenge)throw ve(xe);const i=await qn(th,"getStandardParams",n,o,r.correlationId)(e,t,r,n,o);return Ot(i,O),jt(i,r.codeChallenge,c),Xt(i,{...r.extraQueryParameters,...r.extraParameters}),bo(t,i)}async function nh(e,t,r,n,o,i){if(!n.earJwk)throw es(oi);const s=await th(t,r,n,o,i);Ot(s,M),function(e,t){e.set("ear_jwk",encodeURIComponent(t)),e.set("ear_jwe_crypto","eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0")}(s,n.earJwk),jt(s,n.codeChallenge,c),Xt(s,{...n.extraParameters});const a=new Map;Xt(a,n.extraQueryParameters||{}),Ft(a,n.correlationId);return ih(e,bo(r,a),s)}async function oh(e,t,r,n,o,i){const s=await th(t,r,n,o,i);Ot(s,O),jt(s,n.codeChallenge,n.codeChallengeMethod||c),Xt(s,{...n.extraParameters});const a=new Map;Xt(a,n.extraQueryParameters||{}),Ft(a,n.correlationId);return ih(e,bo(r,a),s)}function ih(e,t,r){const n=e.createElement("form");return n.method="post",n.action=t,r.forEach(((t,r)=>{const o=e.createElement("input");o.hidden=!0,o.name=r,o.value=t,n.appendChild(o)})),e.body.appendChild(n),n}async function sh(e,t,r,n,o,i,s,a,c,h){if(a.verbose("11qcow",e.correlationId),!h)throw es(Ki);const l=new ja(a,c),d=new Yc(n,o,l,a,s,n.system.navigationClient,r,c,h,t,i,e.correlationId),{userRequestState:u}=eo(l.base64Decode,e.state);return qn(d.acquireToken.bind(d),Do,a,c,e.correlationId)({...e,state:u,prompt:void 0})}async function ah(e,t,r,n,o,i,s,a,c,h,l,d){if(ao.removeThrottle(s,o.auth.clientId,e),eh(t,e.correlationId,l),t.accountId)return qn(sh,Qo,h,l,e.correlationId)(e,t.accountId,n,o,s,a,c,h,l,d);const u={...e,code:t.code||"",codeVerifier:r},g=new Wc(i,s,u,h,l);return await qn(g.handleCodeResponse.bind(g),"handleCodeResponse",h,l,e.correlationId)(t,e,n)}async function ch(e,t,r,n,o,i,s,a,c,h,l){if(ao.removeThrottle(i,n.auth.clientId,e),eh(t,e.correlationId,h),Ao(t,e.state),!t.ear_jwe)throw es(ii);if(!e.earJwk)throw es(oi);const d=JSON.parse(await qn(ca,"decryptEarResponse",c,h,e.correlationId)(e.earJwk,t.ear_jwe));if(d.accountId)return qn(sh,Qo,c,h,e.correlationId)(e,d.accountId,r,n,i,s,a,c,h,l);const u=new to(n.auth.clientId,i,new ja(c,h),c,h,null,null);u.validateTokenResponse(d,e.correlationId);const g={code:"",state:e.state,nonce:e.nonce,client_info:d.client_info,cloud_graph_host_name:d.cloud_graph_host_name,cloud_instance_host_name:d.cloud_instance_host_name,cloud_instance_name:d.cloud_instance_name,msgraph_host:d.msgraph_host};return await qn(u.handleServerTokenResponse.bind(u),Rn,c,h,e.correlationId)(d,o,hn(),e,r,g,void 0,void 0,void 0,void 0)}async function hh(e,t,r){const n=xn(lh,"generateCodeVerifier",t,e,r)(e,t,r);return{verifier:n,challenge:await qn(dh,"generateCodeChallengeFromVerifier",t,e,r)(n,e,t,r)}}function lh(e,t,r){try{const n=new Uint8Array(32);xn(na,"getRandomValues",t,e,r)(n);return Ks(n)}catch(e){throw es(ni)}}async function dh(e,t,r,n){try{const o=await qn(ra,"sha256Digest",r,t,n)(e);return Ks(new Uint8Array(o))}catch(e){throw es(ni)}}class uh{navigateInternal(e,t){return uh.defaultNavigateWindow(e,t)}navigateExternal(e,t){return uh.defaultNavigateWindow(e,t)}static defaultNavigateWindow(e,t){return t.noHistory?window.location.replace(e):window.location.assign(e),new Promise(((e,r)=>{setTimeout((()=>{r(es(Qi,"failed_to_redirect"))}),t.timeout)}))}}class gh{async sendGetRequestAsync(e,t){let r,n={},o=0;const i=ph(t);try{r=await fetch(e,{method:Is,headers:i})}catch(e){throw ho(es(window.navigator.onLine?Ri:Ei),void 0,void 0,e)}n=mh(r.headers);try{return o=r.status,{headers:n,body:await r.json(),status:o}}catch(e){throw ho(es(Oi),o,n,e)}}async sendPostRequestAsync(e,t){const r=t&&t.body||"",n=ph(t);let o,i=0,s={};try{o=await fetch(e,{method:Cs,headers:n,body:r})}catch(e){throw ho(es(window.navigator.onLine?Pi:Ei),void 0,void 0,e)}s=mh(o.headers);try{return i=o.status,{headers:s,body:await o.json(),status:i}}catch(e){throw ho(es(Oi),i,s,e)}}}function ph(e){try{const t=new Headers;if(!e||!e.headers)return t;const r=e.headers;return Object.entries(r).forEach((([e,r])=>{t.append(e,r)})),t}catch(e){throw ho(es(Ji),void 0,void 0,e)}}function mh(e){try{const t={};return e.forEach(((e,r)=>{t[r]=e})),t}catch(e){throw es(Wi)}}function fh({auth:r,cache:n,system:o,experimental:c,telemetry:h},l){const d={clientId:"",authority:`${t}`,knownAuthorities:[],cloudDiscoveryMetadata:"",authorityMetadata:"",redirectUri:"undefined"!=typeof window&&window.location?window.location.href.split("?")[0].split("#")[0]:"",postLogoutRedirectUri:"",clientCapabilities:[],OIDCOptions:{responseMode:x.FRAGMENT,defaultScopes:[i,s,a]},azureCloudOptions:{azureCloudInstance:yr.None,tenant:""},instanceAware:!1,isMcp:!1},u={cacheLocation:ws.SessionStorage,cacheRetentionDays:5},g={loggerCallback:()=>{},logLevel:e.LogLevel.Info,piiLoggingEnabled:!1},p={...{...Yr,loggerOptions:g,networkClient:l?new gh:ko,navigationClient:new uh,popupBridgeTimeout:o?.popupBridgeTimeout||6e4,iframeBridgeTimeout:o?.iframeBridgeTimeout||1e4,redirectNavigationTimeout:3e4,allowRedirectInIframe:!1,navigatePopups:!0,allowPlatformBroker:!1,nativeBrokerHandshakeTimeout:o?.nativeBrokerHandshakeTimeout||2e3,protocolMode:Kr.AAD},...o,loggerOptions:o?.loggerOptions||g},m={application:{appName:"",appVersion:""},client:new Xr};if(o?.protocolMode!==Kr.OIDC&&r?.OIDCOptions){new pr(p.loggerOptions).warning(JSON.stringify(ve(Ke)),"")}if(o?.protocolMode&&o.protocolMode===Kr.OIDC&&p?.allowPlatformBroker)throw ve(Be);return{auth:{...d,...r,OIDCOptions:{...d.OIDCOptions,...r?.OIDCOptions}},cache:{...u,...n},system:p,experimental:{iframeTimeoutTelemetry:!1,...c},telemetry:{...m,...h}}}class yh{constructor(e,t,r,n){this.logger=e,this.handshakeTimeoutMs=t,this.extensionId=n,this.resolvers=new Map,this.handshakeResolvers=new Map,this.messageChannel=new MessageChannel,this.windowListener=this.onWindowMessage.bind(this),this.performanceClient=r,this.handshakeEvent=r.startMeasurement("nativeMessageHandlerHandshake"),this.platformAuthType=gs}async sendMessage(e){this.logger.trace("0on4p2",e.correlationId);const t={method:fs,request:e},r={channel:ss,extensionId:this.extensionId,responseId:ia(),body:t};this.logger.trace("1qadfi",e.correlationId),this.logger.tracePii("1xm533",e.correlationId),this.messageChannel.port1.postMessage(r);const n=await new Promise(((e,t)=>{this.resolvers.set(r.responseId,{resolve:e,reject:t})}));return this.validatePlatformBrokerResponse(n)}static async createProvider(e,t,r,n){e.trace("15zfnw",n);try{const o=new yh(e,t,r,as);return await o.sendHandshakeRequest(n),o}catch(o){const i=new yh(e,t,r);return await i.sendHandshakeRequest(n),i}}async sendHandshakeRequest(e){this.logger.trace("1dpg9o",e),window.addEventListener("message",this.windowListener,!1);const t={channel:ss,extensionId:this.extensionId,responseId:ia(),body:{method:ps}};return this.handshakeEvent.add({extensionId:this.extensionId,extensionHandshakeTimeoutMs:this.handshakeTimeoutMs}),this.messageChannel.port1.onmessage=e=>{this.onChannelMessage(e)},window.postMessage(t,window.origin,[this.messageChannel.port2]),new Promise(((e,r)=>{this.handshakeResolvers.set(t.responseId,{resolve:e,reject:r}),this.timeoutId=window.setTimeout((()=>{window.removeEventListener("message",this.windowListener,!1),this.messageChannel.port1.close(),this.messageChannel.port2.close(),this.handshakeEvent.end({extensionHandshakeTimedOut:!0,success:!1}),r(es(Di)),this.handshakeResolvers.delete(t.responseId)}),this.handshakeTimeoutMs)}))}onWindowMessage(e){const t=Ha();if(this.logger.trace("0jpn5u",t),e.source!==window)return;const r=e.data;if(r.channel&&r.channel===ss&&(!r.extensionId||r.extensionId===this.extensionId)&&r.body.method===ps){const e=this.handshakeResolvers.get(r.responseId);if(!e)return void this.logger.trace("07buhm",t);this.logger.verbose(r.extensionId?"0xrkug":"No extension installed",t),clearTimeout(this.timeoutId),this.messageChannel.port1.close(),this.messageChannel.port2.close(),window.removeEventListener("message",this.windowListener,!1),this.handshakeEvent.end({success:!1,extensionInstalled:!1}),e.reject(es(Fi))}}onChannelMessage(e){const t=Ha();this.logger.trace("1py8yf",t);const r=e.data,n=this.resolvers.get(r.responseId),o=this.handshakeResolvers.get(r.responseId);try{const e=r.body.method;if(e===ys){if(!n)return;const e=r.body.response;if(this.logger.trace("19hpgm",t),this.logger.tracePii("179a24",t),"Success"!==e.status)n.reject(Vc(e.code,e.description,e.ext));else{if(!e.result)throw Ie(Eo,"Event does not contain result.");e.result.code&&e.result.description?n.reject(Vc(e.result.code,e.result.description,e.result.ext)):n.resolve(e.result)}this.resolvers.delete(r.responseId)}else if(e===ms){if(!o)return void this.logger.trace("082qnt",t);clearTimeout(this.timeoutId),window.removeEventListener("message",this.windowListener,!1),this.extensionId=r.extensionId,this.extensionVersion=r.body.version,this.logger.verbose("0yf5ib",t),this.handshakeEvent.end({extensionInstalled:!0,success:!0}),o.resolve(),this.handshakeResolvers.delete(r.responseId)}}catch(e){this.logger.error("0xf978",t),this.logger.errorPii("04i99o",t),this.logger.errorPii("0xdvsy",t),n?n.reject(e):o&&o.reject(e)}}validatePlatformBrokerResponse(e){if(e.hasOwnProperty("access_token")&&e.hasOwnProperty("id_token")&&e.hasOwnProperty("client_info")&&e.hasOwnProperty("account")&&e.hasOwnProperty("scope")&&e.hasOwnProperty("expires_in"))return e;throw Ie(Eo,"Response missing expected properties.")}getExtensionId(){return this.extensionId}getExtensionVersion(){return this.extensionVersion}getExtensionName(){return this.getExtensionId()===as?"chrome":this.getExtensionId()?.length?"unknown":void 0}}class wh{constructor(e,t,r){this.logger=e,this.performanceClient=t,this.correlationId=r,this.platformAuthType=us}static async createProvider(e,t,r){if(e.trace("12mj4a",r),window.navigator?.platformAuthentication){const n=await window.navigator.platformAuthentication.getSupportedContracts(hs);if(n?.includes(ds))return e.trace("1h5q1r",r),new wh(e,t,r)}}getExtensionId(){return hs}getExtensionVersion(){return""}getExtensionName(){return ls}async sendMessage(e){this.logger.trace("02bcil",e.correlationId);try{const t=this.initializePlatformDOMRequest(e),r=await window.navigator.platformAuthentication.executeGetToken(t);return this.validatePlatformBrokerResponse(r,e.correlationId)}catch(t){throw this.logger.error("11im7g",e.correlationId),t}}initializePlatformDOMRequest(e){this.logger.trace("15d6yv",e.correlationId);const{accountId:t,clientId:r,authority:n,scope:o,redirectUri:i,correlationId:s,state:a,storeInCache:c,embeddedClientId:h,extraParameters:l,...d}=e,u=this.getDOMExtraParams(d,s);return{accountId:t,brokerId:this.getExtensionId(),authority:n,clientId:r,correlationId:s||this.correlationId,extraParameters:{...l,...u},isSecurityTokenService:!1,redirectUri:i,scope:o,state:a,storeInCache:c,embeddedClientId:h}}validatePlatformBrokerResponse(e,t){if(e.hasOwnProperty("isSuccess")){if(e.hasOwnProperty("accessToken")&&e.hasOwnProperty("idToken")&&e.hasOwnProperty("clientInfo")&&e.hasOwnProperty("account")&&e.hasOwnProperty("scopes")&&e.hasOwnProperty("expiresIn"))return this.logger.trace("0h4vei",t),this.convertToPlatformBrokerResponse(e,t);if(e.hasOwnProperty("error")){const r=e;if(!1===r.isSuccess&&r.error&&r.error.code)throw this.logger.trace("0g92vm",t),Vc(r.error.code,r.error.description,{error:parseInt(r.error.errorCode),protocol_error:r.error.protocolError,status:r.error.status,properties:r.error.properties})}}throw Ie(Eo,"Response missing expected properties.")}convertToPlatformBrokerResponse(e,t){this.logger.trace("14913t",t);return{access_token:e.accessToken,id_token:e.idToken,client_info:e.clientInfo,account:e.account,expires_in:e.expiresIn,scope:e.scopes,state:e.state||"",properties:e.properties||{},extendedLifetimeToken:e.extendedLifetimeToken??!1,shr:e.proofOfPossessionPayload}}getDOMExtraParams(e,t){try{const t={};for(const[r,n]of Object.entries(e))n&&(t[r]="object"==typeof n?JSON.stringify(n):String(n));return t}catch(e){return this.logger.error("0eu9o3",t),this.logger.errorPii("17rpl5",t),{}}}}async function Ih(e,t,r,n){e.trace("134j0v",r);const o=function(){let e;try{return e=window[ws.SessionStorage],"true"===e?.getItem(ac)}catch(e){return!1}}();let i;e.trace("04c81g",r);try{o&&(i=await wh.createProvider(e,t,r)),i||(e.trace("0l3na8",r),i=await yh.createProvider(e,n||2e3,t,r))}catch(t){e.trace("0icbd7",t)}return i}function Ch(e,t,r,n,o){if(t.trace("0uko3r",r),!e.system.allowPlatformBroker)return t.trace("04hozs",r),!1;if(!n)return t.trace("0kvv1r",r),!1;if(o)switch(o){case G.BEARER:case G.POP:return t.trace("18tev1",r),!0;default:return t.trace("1dd2nh",r),!1}return!0}class vh extends zc{constructor(e,t,r,n,o,i,s,a,c,h){super(e,t,r,n,o,i,s,c,h),this.nativeStorage=a,this.eventHandler=o}acquireToken(e,t){let r;try{if(r={popupName:this.generatePopupName(e.scopes||p,e.authority||this.config.auth.authority),popupWindowAttributes:e.popupWindowAttributes||{},popupWindowParent:e.popupWindowParent??window},this.performanceClient.addFields({isAsyncPopup:!this.config.system.navigatePopups},this.correlationId),this.config.system.navigatePopups){const n={...e,httpMethod:Bc(e,this.config.system.protocolMode)};return this.logger.verbose("1f9ok3",this.correlationId),r.popup=this.openSizedPopup("about:blank",r),this.acquireTokenPopupAsync(n,r,t)}return this.logger.verbose("162h4u",this.correlationId),this.acquireTokenPopupAsync(e,r,t)}catch(e){return Promise.reject(e)}}logout(e){try{this.logger.verbose("068rup",this.correlationId);const t=this.initializeLogoutRequest(e),r={popupName:this.generateLogoutPopupName(t),popupWindowAttributes:e?.popupWindowAttributes||{},popupWindowParent:e?.popupWindowParent??window},n=e&&e.authority,o=e&&e.mainWindowRedirectUri;return this.config.system.navigatePopups?(this.logger.verbose("1a28da",this.correlationId),r.popup=this.openSizedPopup("about:blank",r),this.logoutPopupAsync(t,r,n,o)):(this.logger.verbose("1phd8u",this.correlationId),this.logoutPopupAsync(t,r,n,o))}catch(e){return Promise.reject(e)}}async acquireTokenPopupAsync(t,r,n){this.logger.verbose("1g77pg",this.correlationId);const o=await qn(jc,Wo,this.logger,this.performanceClient,this.correlationId)(t,e.InteractionType.Popup,this.config,this.browserCrypto,this.browserStorage,this.logger,this.performanceClient,this.correlationId);r.popup&&La(o.authority);const i=Ch(this.config,this.logger,this.correlationId,this.platformAuthProvider,t.authenticationScheme);return o.platformBroker=i,this.config.system.protocolMode===Kr.EAR?this.executeEarFlow(o,r,n):this.executeCodeFlow(o,r,n)}async executeCodeFlow(t,r,n){const o=t.correlationId,i=Lc(Os.acquireTokenPopup,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger),s=n||await qn(hh,Zo,this.logger,this.performanceClient,o)(this.performanceClient,this.logger,o),a={...t,codeChallenge:s.challenge};try{const n=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,o)({serverTelemetryManager:i,requestAuthority:a.authority,requestAzureCloudOptions:a.azureCloudOptions,requestExtraQueryParameters:a.extraQueryParameters,account:a.account});if(a.httpMethod===g)return await this.executeCodeFlowWithPost(a,r,n,s.verifier);{const i=await qn(rh,_n,this.logger,this.performanceClient,o)(this.config,n.authority,a,this.logger,this.performanceClient),c=this.initiateAuthRequest(i,r);this.eventHandler.emitEvent(Tc.POPUP_OPENED,o,e.InteractionType.Popup,{popupWindow:c},null);const h=await _a(this.config.system.popupBridgeTimeout,this.logger,this.browserCrypto,t,this.performanceClient),l=xn($c,Xo,this.logger,this.performanceClient,this.correlationId)(h,this.config.auth.OIDCOptions.responseMode,this.logger,this.correlationId);return await qn(ah,Vo,this.logger,this.performanceClient,o)(t,l,s.verifier,Os.acquireTokenPopup,this.config,n,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}}catch(e){throw r.popup?.close(),e instanceof we&&(e.setCorrelationId(this.correlationId),i.cacheFailedRequest(e)),e}}async executeEarFlow(e,t,r){const{correlationId:n,authority:o,azureCloudOptions:i,extraQueryParameters:s,account:a}=e,c=await qn(Hc,Ho,this.logger,this.performanceClient,n)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,o,i,s,a),h=await qn(aa,ri,this.logger,this.performanceClient,n)(),l=r||await qn(hh,Zo,this.logger,this.performanceClient,n)(this.performanceClient,this.logger,n),d={...e,earJwk:h,codeChallenge:l.challenge},u=t.popup||this.openPopup("about:blank",t);(await nh(u.document,this.config,c,d,this.logger,this.performanceClient)).submit();const g=await qn(_a,jo,this.logger,this.performanceClient,n)(this.config.system.popupBridgeTimeout,this.logger,this.browserCrypto,d,this.performanceClient),p=xn($c,Xo,this.logger,this.performanceClient,this.correlationId)(g,this.config.auth.OIDCOptions.responseMode,this.logger,this.correlationId);if(!p.ear_jwe&&p.code){const t=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,n)({serverTelemetryManager:Lc(Os.acquireTokenPopup,this.config.auth.clientId,n,this.browserStorage,this.logger),requestAuthority:e.authority,requestAzureCloudOptions:e.azureCloudOptions,requestExtraQueryParameters:e.extraQueryParameters,account:e.account,authority:c});return qn(ah,Vo,this.logger,this.performanceClient,n)(d,p,l.verifier,Os.acquireTokenPopup,this.config,t,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}return qn(ch,Go,this.logger,this.performanceClient,n)(d,p,Os.acquireTokenPopup,this.config,c,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}async executeCodeFlowWithPost(e,t,r,n){const o=e.correlationId,i=await qn(Hc,Ho,this.logger,this.performanceClient,o)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger),s=t.popup||this.openPopup("about:blank",t);(await oh(s.document,this.config,i,e,this.logger,this.performanceClient)).submit();const a=await qn(_a,jo,this.logger,this.performanceClient,o)(this.config.system.popupBridgeTimeout,this.logger,this.browserCrypto,e,this.performanceClient),c=xn($c,Xo,this.logger,this.performanceClient,this.correlationId)(a,this.config.auth.OIDCOptions.responseMode,this.logger,this.correlationId);return qn(ah,Vo,this.logger,this.performanceClient,o)(e,c,n,Os.acquireTokenPopup,this.config,r,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}async logoutPopupAsync(t,r,n,o){this.logger.verbose("0b7yrk",this.correlationId),this.eventHandler.emitEvent(Tc.LOGOUT_START,this.correlationId,e.InteractionType.Popup,t);const i=Lc(Os.logoutPopup,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger);try{await Dc(this.browserStorage,this.browserCrypto,this.logger,this.correlationId,t.account);const s=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:i,requestAuthority:n,account:t.account||void 0});try{s.authority.endSessionEndpoint}catch{if(t.account?.homeAccountId&&t.postLogoutRedirectUri&&s.authority.protocolMode===Kr.OIDC){if(this.eventHandler.emitEvent(Tc.LOGOUT_SUCCESS,t.correlationId,e.InteractionType.Popup,t),o){const e={apiId:Os.logoutPopup,timeout:this.config.system.redirectNavigationTimeout,noHistory:!1},t=br.getAbsoluteUrl(o,Ea());await this.navigationClient.navigateInternal(t,e)}return void r.popup?.close()}}const a=s.getLogoutUri(t);this.eventHandler.emitEvent(Tc.LOGOUT_SUCCESS,t.correlationId,e.InteractionType.Popup,t);const c=this.openPopup(a,r);if(this.eventHandler.emitEvent(Tc.POPUP_OPENED,t.correlationId,e.InteractionType.Popup,{popupWindow:c},null),await _a(this.config.system.popupBridgeTimeout,this.logger,this.browserCrypto,t,this.performanceClient).catch((()=>{})),o){const e={apiId:Os.logoutPopup,timeout:this.config.system.redirectNavigationTimeout,noHistory:!1},t=br.getAbsoluteUrl(o,Ea());this.logger.verbose("0qcur2",this.correlationId),this.logger.verbosePii("0oj7lk",this.correlationId),await this.navigationClient.navigateInternal(t,e)}else this.logger.verbose("03zgcf",this.correlationId)}catch(t){throw r.popup?.close(),t instanceof we&&(t.setCorrelationId(this.correlationId),i.cacheFailedRequest(t)),this.eventHandler.emitEvent(Tc.LOGOUT_FAILURE,this.correlationId,e.InteractionType.Popup,null,t),this.eventHandler.emitEvent(Tc.LOGOUT_END,this.correlationId,e.InteractionType.Popup),t}this.eventHandler.emitEvent(Tc.LOGOUT_END,this.correlationId,e.InteractionType.Popup)}initiateAuthRequest(e,t){if(e)return this.logger.infoPii("1kcr9k",this.correlationId),this.openPopup(e,t);throw this.logger.error("1l7hyp",this.correlationId),es(ai)}openPopup(e,t){try{let r;if(t.popup?(r=t.popup,this.logger.verbosePii("0cgeo7",this.correlationId),r.location.assign(e)):void 0===t.popup&&(this.logger.verbosePii("0c2awd",this.correlationId),r=this.openSizedPopup(e,t)),!r)throw es(fi);return r.focus&&r.focus(),this.currentWindow=r,r}catch(e){throw this.logger.error("0dxfb9",this.correlationId),es(mi)}}openSizedPopup(e,{popupName:t,popupWindowAttributes:r,popupWindowParent:n}){const o=n.screenLeft?n.screenLeft:n.screenX,i=n.screenTop?n.screenTop:n.screenY,s=n.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,a=n.innerHeight||document.documentElement.clientHeight||document.body.clientHeight;let c=r.popupSize?.width,h=r.popupSize?.height,l=r.popupPosition?.top,d=r.popupPosition?.left;return(!c||c<0||c>s)&&(this.logger.verbose("08vfmo",this.correlationId),c=rs),(!h||h<0||h>a)&&(this.logger.verbose("09cxa0",this.correlationId),h=ns),(!l||l<0||l>a)&&(this.logger.verbose("1qh4wo",this.correlationId),l=Math.max(0,a/2-ns/2+i)),(!d||d<0||d>s)&&(this.logger.verbose("1sz3en",this.correlationId),d=Math.max(0,s/2-rs/2+o)),n.open(e,t,`width=${c}, height=${h}, top=${l}, left=${d}, scrollbars=yes`)}generatePopupName(e,t){return`${os}.${this.config.auth.clientId}.${e.join("-")}.${t}.${this.correlationId}`}generateLogoutPopupName(e){const t=e.account&&e.account.homeAccountId;return`${os}.${this.config.auth.clientId}.${t}.${this.correlationId}`}}class kh extends zc{constructor(e,t,r,n,o,i,s,a,c,h){super(e,t,r,n,o,i,s,c,h),this.nativeStorage=a}async acquireToken(t){const r=await qn(jc,Wo,this.logger,this.performanceClient,this.correlationId)(t,e.InteractionType.Redirect,this.config,this.browserCrypto,this.browserStorage,this.logger,this.performanceClient,this.correlationId);r.platformBroker=Ch(this.config,this.logger,this.correlationId,this.platformAuthProvider,t.authenticationScheme);const n=t=>{t.persisted&&(this.logger.verbose("0udvtt",this.correlationId),this.browserStorage.resetRequestCache(this.correlationId),this.eventHandler.emitEvent(Tc.RESTORE_FROM_BFCACHE,this.correlationId,e.InteractionType.Redirect))},o=this.getRedirectStartPage(t.redirectStartPage);this.logger.verbosePii("0zao0a",this.correlationId),this.browserStorage.setTemporaryCache(Ts,o,!0),window.addEventListener("pageshow",n);try{this.config.system.protocolMode===Kr.EAR?await this.executeEarFlow(r):await this.executeCodeFlow(r)}catch(e){throw e instanceof we&&e.setCorrelationId(this.correlationId),window.removeEventListener("pageshow",n),e}}async executeCodeFlow(e){const t=e.correlationId,r=Lc(Os.acquireTokenRedirect,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger),n=await qn(hh,Zo,this.logger,this.performanceClient,t)(this.performanceClient,this.logger,t),o={...e,codeChallenge:n.challenge};this.browserStorage.cacheAuthorizeRequest(o,this.correlationId,n.verifier);try{if(o.httpMethod===g)return await this.executeCodeFlowWithPost(o);{const t=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:r,requestAuthority:o.authority,requestAzureCloudOptions:o.azureCloudOptions,requestExtraQueryParameters:o.extraQueryParameters,account:o.account}),n=await qn(rh,_n,this.logger,this.performanceClient,e.correlationId)(this.config,t.authority,o,this.logger,this.performanceClient);return await this.initiateAuthRequest(n)}}catch(e){throw e instanceof we&&(e.setCorrelationId(this.correlationId),r.cacheFailedRequest(e)),e}}async executeEarFlow(e){const{correlationId:t,authority:r,azureCloudOptions:n,extraQueryParameters:o,account:i}=e,s=await qn(Hc,Ho,this.logger,this.performanceClient,t)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,r,n,o,i),a=await qn(aa,ri,this.logger,this.performanceClient,t)(),c=await qn(hh,Zo,this.logger,this.performanceClient,t)(this.performanceClient,this.logger,t),h={...e,earJwk:a,codeChallenge:c.challenge};this.browserStorage.cacheAuthorizeRequest(h,this.correlationId,c.verifier);return(await nh(document,this.config,s,h,this.logger,this.performanceClient)).submit(),new Promise(((e,t)=>{setTimeout((()=>{t(es(Qi,"failed_to_redirect"))}),this.config.system.redirectNavigationTimeout)}))}async executeCodeFlowWithPost(e){const t=e.correlationId,r=await qn(Hc,Ho,this.logger,this.performanceClient,t)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger);this.browserStorage.cacheAuthorizeRequest(e,this.correlationId);return(await oh(document,this.config,r,e,this.logger,this.performanceClient)).submit(),new Promise(((e,t)=>{setTimeout((()=>{t(es(Qi,"failed_to_redirect"))}),this.config.system.redirectNavigationTimeout)}))}async handleRedirectPromise(e,t,r,n){const o=Lc(Os.handleRedirectPromise,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger),i=n?.navigateToLoginRequestUrl??!0;try{const[s,a]=this.getRedirectResponse(n?.hash||"");if(!s)return this.logger.info("1qmv0q",this.correlationId),this.browserStorage.resetRequestCache(this.correlationId),"back_forward"!==function(){if("undefined"==typeof window||void 0===window.performance||"function"!=typeof window.performance.getEntriesByType)return;const e=window.performance.getEntriesByType("navigation"),t=e.length?e[0]:void 0;return t?.type}()?r.event.errorCode="no_server_response":this.logger.verbose("1eqegq",this.correlationId),null;const c=this.browserStorage.getTemporaryCache(Ts,this.correlationId,!0)||"",h=hr(c);if(h===hr(window.location.href)&&i){this.logger.verbose("11yred",this.correlationId),c.indexOf("#")>-1&&ka(c);return await this.handleResponse(s,e,t,o)}if(!i)return this.logger.verbose("0v4sdv",this.correlationId),await this.handleResponse(s,e,t,o);if(!Ta()||this.config.system.allowRedirectInIframe){this.browserStorage.setTemporaryCache(bs,a,!0);const r={apiId:Os.handleRedirectPromise,timeout:this.config.system.redirectNavigationTimeout,noHistory:!0};let n=!0;if(c&&"null"!==c)this.logger.verbose("08jpy1",this.correlationId),n=await this.navigationClient.navigateInternal(c,r);else{const e=Pa();this.browserStorage.setTemporaryCache(Ts,e,!0),this.logger.warning("1dutq1",this.correlationId),n=await this.navigationClient.navigateInternal(e,r)}if(!n)return await this.handleResponse(s,e,t,o)}return null}catch(e){throw e instanceof we&&(e.setCorrelationId(this.correlationId),o.cacheFailedRequest(e)),e}}getRedirectResponse(t){this.logger.verbose("1c5i8m",this.correlationId);let r=t;r||(r=this.config.auth.OIDCOptions.responseMode===x.QUERY?window.location.search:window.location.hash);let n=ar(r);if(n){try{Jc(n,this.browserCrypto,e.InteractionType.Redirect)}catch(e){return e instanceof we&&this.logger.error("0bkq6p",this.correlationId),[null,""]}return va(window),this.logger.verbose("00uvho",this.correlationId),[n,r]}const o=this.browserStorage.getTemporaryCache(bs,this.correlationId,!0);return this.browserStorage.removeItem(this.browserStorage.generateCacheKey(bs)),o&&(n=ar(o),n)?(this.logger.verbose("001671",this.correlationId),[n,o]):[null,""]}async handleResponse(e,t,r,n){if(!e.state)throw es(hi);const{authority:o,azureCloudOptions:i,extraQueryParameters:s,account:a}=t;if(e.ear_jwe){const r=await qn(Hc,Ho,this.logger,this.performanceClient,t.correlationId)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,o,i,s,a);return qn(ch,Go,this.logger,this.performanceClient,t.correlationId)(t,e,Os.acquireTokenRedirect,this.config,r,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}const c=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:n,requestAuthority:t.authority});return qn(ah,Vo,this.logger,this.performanceClient,t.correlationId)(t,e,r,Os.acquireTokenRedirect,this.config,c,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}async initiateAuthRequest(e){if(this.logger.verbose("0yaw2e",this.correlationId),e){this.logger.infoPii("1luf83",this.correlationId);const t={apiId:Os.acquireTokenRedirect,timeout:this.config.system.redirectNavigationTimeout,noHistory:!1},r=this.config.auth.onRedirectNavigate;if("function"==typeof r){this.logger.verbose("1nehvl",this.correlationId);return!1!==r(e)?(this.logger.verbose("1a0jxh",this.correlationId),void await this.navigationClient.navigateExternal(e,t)):void this.logger.verbose("09k5h5",this.correlationId)}return this.logger.verbose("0klwf7",this.correlationId),void await this.navigationClient.navigateExternal(e,t)}throw this.logger.info("0rlh4e",this.correlationId),es(ai)}async logout(t){this.logger.verbose("1rkurh",this.correlationId);const r=this.initializeLogoutRequest(t),n=Lc(Os.logout,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger);try{this.eventHandler.emitEvent(Tc.LOGOUT_START,this.correlationId,e.InteractionType.Redirect,t),await Dc(this.browserStorage,this.browserCrypto,this.logger,this.correlationId,r.account);const o={apiId:Os.logout,timeout:this.config.system.redirectNavigationTimeout,noHistory:!1},i=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:n,requestAuthority:t&&t.authority,requestExtraQueryParameters:t?.extraQueryParameters,account:t&&t.account||void 0});if(i.authority.protocolMode===Kr.OIDC)try{i.authority.endSessionEndpoint}catch{if(r.account?.homeAccountId)return void this.eventHandler.emitEvent(Tc.LOGOUT_SUCCESS,this.correlationId,e.InteractionType.Redirect,r)}const s=i.getLogoutUri(r);r.account?.homeAccountId&&this.eventHandler.emitEvent(Tc.LOGOUT_SUCCESS,this.correlationId,e.InteractionType.Redirect,r);const a=this.config.auth.onRedirectNavigate;if("function"!=typeof a)return this.browserStorage.getInteractionInProgress()||this.browserStorage.setInteractionInProgress(!0,ks),void await this.navigationClient.navigateExternal(s,o);if(!1!==a(s))return this.logger.verbose("06v57e",this.correlationId),this.browserStorage.getInteractionInProgress()||this.browserStorage.setInteractionInProgress(!0,ks),void await this.navigationClient.navigateExternal(s,o);this.browserStorage.setInteractionInProgress(!1),this.logger.verbose("0xqes1",this.correlationId)}catch(t){throw t instanceof we&&(t.setCorrelationId(this.correlationId),n.cacheFailedRequest(t)),this.eventHandler.emitEvent(Tc.LOGOUT_FAILURE,this.correlationId,e.InteractionType.Redirect,null,t),this.eventHandler.emitEvent(Tc.LOGOUT_END,this.correlationId,e.InteractionType.Redirect),t}this.eventHandler.emitEvent(Tc.LOGOUT_END,this.correlationId,e.InteractionType.Redirect)}getRedirectStartPage(e){const t=e||window.location.href;return br.getAbsoluteUrl(t,Ea())}}async function Th(e,t,r,n){if(!e)throw r.info("1l7hyp",n),es(ai);return xn(Sh,"silentHandlerLoadFrameSync",r,t,n)(e)}async function bh(e,t,r,n,o){const i=_h();if(!i.contentDocument)throw"No document associated with iframe!";return(await oh(i.contentDocument,e,t,r,n,o)).submit(),i}async function Ah(e,t,r,n,o){const i=_h();if(!i.contentDocument)throw"No document associated with iframe!";return(await nh(i.contentDocument,e,t,r,n,o)).submit(),i}function Sh(e){const t=_h();return t.src=e,t}function _h(){const e=document.createElement("iframe");return e.className="msalSilentIframe",e.style.visibility="hidden",e.style.position="absolute",e.style.width=e.style.height="0",e.style.border="0",e.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms"),e.setAttribute("allow","local-network-access *"),document.body.appendChild(e),e}function Eh(e){document.body===e.parentNode&&document.body.removeChild(e)}class Ph extends zc{constructor(e,t,r,n,o,i,s,a,c,h,l){super(e,t,r,n,o,i,a,h,l),this.apiId=s,this.nativeStorage=c}async acquireToken(t){t.loginHint||t.sid||t.account&&t.account.username||this.logger.warning("1kl318",this.correlationId);const r={...t};r.prompt?r.prompt!==R.NONE&&r.prompt!==R.NO_SESSION&&(this.logger.warning("0bmctg",this.correlationId),r.prompt=R.NONE):r.prompt=R.NONE;const n=await qn(jc,Wo,this.logger,this.performanceClient,this.correlationId)(r,e.InteractionType.Silent,this.config,this.browserCrypto,this.browserStorage,this.logger,this.performanceClient,this.correlationId);return n.platformBroker=Ch(this.config,this.logger,this.correlationId,this.platformAuthProvider,n.authenticationScheme),La(n.authority),this.config.system.protocolMode===Kr.EAR?this.executeEarFlow(n):this.executeCodeFlow(n)}async executeCodeFlow(e){let t;const r=Lc(this.apiId,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger);try{return t=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,e.correlationId)({serverTelemetryManager:r,requestAuthority:e.authority,requestAzureCloudOptions:e.azureCloudOptions,requestExtraQueryParameters:e.extraQueryParameters,account:e.account}),await qn(this.silentTokenHelper.bind(this),Bo,this.logger,this.performanceClient,e.correlationId)(t,e)}catch(n){if(n instanceof we&&(n.setCorrelationId(this.correlationId),r.cacheFailedRequest(n)),!(t&&n instanceof we&&n.errorCode===ts))throw n;return this.performanceClient.addFields({retryError:n.errorCode},this.correlationId),await qn(this.silentTokenHelper.bind(this),Bo,this.logger,this.performanceClient,this.correlationId)(t,e)}}async executeEarFlow(e){const{correlationId:t,authority:r,azureCloudOptions:n,extraQueryParameters:o,account:i}=e,s=await qn(Hc,Ho,this.logger,this.performanceClient,t)(this.config,this.correlationId,this.performanceClient,this.browserStorage,this.logger,r,n,o,i),a=await qn(aa,ri,this.logger,this.performanceClient,t)(),c=await qn(hh,Zo,this.logger,this.performanceClient,t)(this.performanceClient,this.logger,t),h={...e,earJwk:a,codeChallenge:c.challenge},l=await qn(Ah,zo,this.logger,this.performanceClient,t)(this.config,s,h,this.logger,this.performanceClient),d=this.config.auth.OIDCOptions.responseMode;let u;try{u=await qn(_a,jo,this.logger,this.performanceClient,t)(this.config.system.iframeBridgeTimeout,this.logger,this.browserCrypto,e,this.performanceClient,this.config.experimental)}finally{xn(Eh,Yo,this.logger,this.performanceClient,t)(l)}const g=xn($c,Xo,this.logger,this.performanceClient,t)(u,d,this.logger,this.correlationId);if(!g.ear_jwe&&g.code){const r=await qn(this.createAuthCodeClient.bind(this),$o,this.logger,this.performanceClient,t)({serverTelemetryManager:Lc(this.apiId,this.config.auth.clientId,t,this.browserStorage,this.logger),requestAuthority:e.authority,requestAzureCloudOptions:e.azureCloudOptions,requestExtraQueryParameters:e.extraQueryParameters,account:e.account,authority:s});return qn(ah,Vo,this.logger,this.performanceClient,t)(h,g,c.verifier,this.apiId,this.config,r,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}return qn(ch,Go,this.logger,this.performanceClient,t)(h,g,this.apiId,this.config,s,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}logout(){return Promise.reject(es(ki))}async silentTokenHelper(e,t){const r=t.correlationId,n=await qn(hh,Zo,this.logger,this.performanceClient,r)(this.performanceClient,this.logger,r),o={...t,codeChallenge:n.challenge};let i;if(t.httpMethod===g)i=await qn(bh,zo,this.logger,this.performanceClient,r)(this.config,e.authority,o,this.logger,this.performanceClient);else{const t=await qn(rh,_n,this.logger,this.performanceClient,r)(this.config,e.authority,o,this.logger,this.performanceClient);i=await qn(Th,zo,this.logger,this.performanceClient,r)(t,this.performanceClient,this.logger,r)}const s=this.config.auth.OIDCOptions.responseMode;let a;try{a=await qn(_a,jo,this.logger,this.performanceClient,r)(this.config.system.iframeBridgeTimeout,this.logger,this.browserCrypto,t,this.performanceClient,this.config.experimental)}finally{xn(Eh,Yo,this.logger,this.performanceClient,r)(i)}const c=xn($c,Xo,this.logger,this.performanceClient,r)(a,s,this.logger,this.correlationId);return qn(ah,Vo,this.logger,this.performanceClient,r)(t,c,n.verifier,this.apiId,this.config,e,this.browserStorage,this.nativeStorage,this.eventHandler,this.logger,this.performanceClient,this.platformAuthProvider)}}class Rh extends zc{async acquireToken(e){const t=await qn(Fc,Ko,this.logger,this.performanceClient,e.correlationId)(e,this.config,this.performanceClient,this.logger,this.correlationId),r={...e,...t};e.redirectUri&&(r.redirectUri=Uc(e.redirectUri,this.config.auth.redirectUri,this.logger,this.correlationId));const n=Lc(Os.acquireTokenSilent_silentFlow,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger),o=await this.createRefreshTokenClient({serverTelemetryManager:n,authorityUrl:r.authority,azureCloudOptions:r.azureCloudOptions,account:r.account});return qn(o.acquireTokenByRefreshToken.bind(o),"refreshTokenClientAcquireTokenByRefreshToken",this.logger,this.performanceClient,e.correlationId)(r,Os.acquireTokenSilent_silentFlow).catch((e=>{throw e.setCorrelationId(this.correlationId),n.cacheFailedRequest(e),e}))}logout(){return Promise.reject(es(ki))}async createRefreshTokenClient(e){const t=await qn(this.getClientConfiguration.bind(this),Jo,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:e.serverTelemetryManager,requestAuthority:e.authorityUrl,requestAzureCloudOptions:e.azureCloudOptions,requestExtraQueryParameters:e.extraQueryParameters,account:e.account});return new Co(t,this.performanceClient)}}class Oh extends Io{constructor(e,t){super(e,t),this.includeRedirectUri=!1}}class Mh extends zc{constructor(e,t,r,n,o,i,s,a,c,h){super(e,t,r,n,o,i,a,c,h),this.apiId=s}async acquireToken(t){if(!t.code)throw es(qi);const r=await qn(jc,Wo,this.logger,this.performanceClient,this.correlationId)(t,e.InteractionType.Silent,this.config,this.browserCrypto,this.browserStorage,this.logger,this.performanceClient,this.correlationId),n=Lc(this.apiId,this.config.auth.clientId,this.correlationId,this.browserStorage,this.logger);try{const e={...r,code:t.code},o=await qn(this.getClientConfiguration.bind(this),Jo,this.logger,this.performanceClient,this.correlationId)({serverTelemetryManager:n,requestAuthority:r.authority,requestAzureCloudOptions:r.azureCloudOptions,requestExtraQueryParameters:r.extraQueryParameters,account:r.account}),i=new Oh(o,this.performanceClient);this.logger.verbose("1uic5e",this.correlationId);const s=new Wc(i,this.browserStorage,e,this.logger,this.performanceClient);return await qn(s.handleCodeResponseFromServer.bind(s),En,this.logger,this.performanceClient,this.correlationId)({code:t.code,msgraph_host:t.msGraphHost,cloud_graph_host_name:t.cloudGraphHostName,cloud_instance_host_name:t.cloudInstanceHostName},r,this.apiId,!1)}catch(e){throw e instanceof we&&(e.setCorrelationId(this.correlationId),n.cacheFailedRequest(e)),e}}logout(){return Promise.reject(es(ki))}}function xh(e,t,r,n){try{Na(e),So(r.auth.isMcp,n)}catch(e){throw t.end({success:!1},e,n.account),e}}class qh{constructor(e){this.operatingContext=e,this.isBrowserEnvironment=this.operatingContext.isBrowserEnvironment(),this.config=e.getConfig(),this.initialized=!1,this.logger=this.operatingContext.getLogger(),this.networkClient=this.config.system.networkClient,this.navigationClient=this.config.system.navigationClient,this.redirectResponse=new Map,this.hybridAuthCodeResponses=new Map,this.performanceClient=this.config.telemetry.client,this.browserCrypto=this.isBrowserEnvironment?new ja(this.logger,this.performanceClient):lr,this.eventHandler=new qc(this.logger),this.browserStorage=this.isBrowserEnvironment?new _c(this.config.auth.clientId,this.config.cache,this.browserCrypto,this.logger,this.performanceClient,this.eventHandler,yo(this.config.auth)):Pc(this.config.auth.clientId,this.logger,this.performanceClient,this.eventHandler);const t={cacheLocation:ws.MemoryStorage,cacheRetentionDays:5};this.nativeInternalStorage=new _c(this.config.auth.clientId,t,this.browserCrypto,this.logger,this.performanceClient,this.eventHandler),this.activeSilentTokenRequests=new Map,this.trackStateChange=this.trackStateChange.bind(this),this.trackStateChangeWithMeasurement=this.trackStateChangeWithMeasurement.bind(this)}static async createController(e,t){const r=new qh(e);return await r.initialize(t),r}trackStateChange(e,t){e&&("visibilitychange"===t.type?(this.logger.info("16v6hv",e),this.performanceClient.incrementFields({visibilityChangeCount:1},e)):"online"===t.type?(this.logger.info("0zirfd",e),this.performanceClient.incrementFields({onlineStatusChangeCount:1},e)):"offline"===t.type&&(this.logger.info("1xk9ef",e),this.performanceClient.incrementFields({onlineStatusChangeCount:1},e)))}async initialize(e){const t=this.getRequestCorrelationId(e);if(this.logger.trace("1f7joy",t),this.initialized)return void this.logger.info("061m5x",t);if(!this.isBrowserEnvironment)return this.logger.info("19fvpi",t),this.initialized=!0,void this.eventHandler.emitEvent(Tc.INITIALIZE_END,t);const r=this.config.system.allowPlatformBroker,n=this.performanceClient.startMeasurement(Ya,t);if(this.eventHandler.emitEvent(Tc.INITIALIZE_START,t),this.logMultipleInstances(n,t),n.add({isMcp:this.config.auth.isMcp}),await qn(this.browserStorage.initialize.bind(this.browserStorage),"initializeCache",this.logger,this.performanceClient,t)(t),r)try{this.platformAuthProvider=await Ih(this.logger,this.performanceClient,t,this.config.system.nativeBrokerHandshakeTimeout)}catch(e){this.logger.verbose(e,t)}this.config.cache.cacheLocation===ws.LocalStorage&&this.eventHandler.subscribeCrossTab(),!this.config.system.navigatePopups&&await this.preGeneratePkceCodes(t),this.initialized=!0,this.eventHandler.emitEvent(Tc.INITIALIZE_END,t),n.end({allowPlatformBroker:r,success:!0})}async handleRedirectPromise(e){if(this.logger.verbose("02l8bm",""),qa(this.initialized),this.isBrowserEnvironment){const t=e?.hash||"";let r=this.redirectResponse.get(t);return void 0===r?(r=this.handleRedirectPromiseInternal(e),this.redirectResponse.set(t,r),this.logger.verbose("1wn9kp","")):this.logger.verbose("0w0gm3",""),r}return this.logger.verbose("12xi63",""),null}async handleRedirectPromiseInternal(t){if(!this.browserStorage.isInteractionInProgress(!0))return this.logger.info("0le6uv",""),null;const r=this.browserStorage.getInteractionInProgress()?.type;if(r===ks)return this.logger.verbose("1ywcv2",""),this.browserStorage.setInteractionInProgress(!1),Promise.resolve(null);const n=this.getAllAccounts(),o=this.browserStorage.getCachedNativeRequest(),i=o&&this.platformAuthProvider&&!t?.hash;let s,a;try{if(i&&this.platformAuthProvider){const t=o?.correlationId||"";this.eventHandler.emitEvent(Tc.HANDLE_REDIRECT_START,t,e.InteractionType.Redirect),s=this.performanceClient.startMeasurement(Va,t),this.logger.trace("12v7is",t);const r=new Yc(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,Os.handleRedirectPromise,this.performanceClient,this.platformAuthProvider,o.accountId,this.nativeInternalStorage,o.correlationId);a=qn(r.handleRedirectPromise.bind(r),"handleNativeRedirectPromise",this.logger,this.performanceClient,s.event.correlationId)(this.performanceClient,s.event.correlationId)}else{const[r,n]=this.browserStorage.getCachedRequest(""),o=r.correlationId;this.eventHandler.emitEvent(Tc.HANDLE_REDIRECT_START,o,e.InteractionType.Redirect),s=this.performanceClient.startMeasurement(Va,o),this.logger.trace("0znzs5",o);const i=this.createRedirectClient(o);a=qn(i.handleRedirectPromise.bind(i),"handleRedirectPromise",this.logger,this.performanceClient,s.event.correlationId)(r,n,s,t)}}catch(e){throw this.browserStorage.resetRequestCache(""),e}return a.then((t=>{if(t){this.browserStorage.resetRequestCache(t.correlationId),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,t.correlationId,e.InteractionType.Redirect,t),this.logger.verbose("0ui8f5",t.correlationId);n.length{this.browserStorage.resetRequestCache(s.event.correlationId);const r=t;throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,s.event.correlationId,e.InteractionType.Redirect,null,r),this.eventHandler.emitEvent(Tc.HANDLE_REDIRECT_END,s.event.correlationId,e.InteractionType.Redirect),s.end({success:!1},r),t}))}async acquireTokenRedirect(t){const r=this.getRequestCorrelationId(t);this.logger.verbose("0os66p",r);const n=this.performanceClient.startMeasurement(Qa,r);n.add({scenarioId:t.scenarioId});const o=this.config.auth.onRedirectNavigate;this.config.auth.onRedirectNavigate=e=>{const r="function"==typeof o?o(e):void 0;return n.add({navigateCallbackResult:!1!==r}),n.event=n.end({success:!0},void 0,t.account)||n.event,r};try{let o;if(Ua(this.initialized,this.config),So(this.config.auth.isMcp,t),this.browserStorage.setInteractionInProgress(!0,vs),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,r,e.InteractionType.Redirect,t),this.platformAuthProvider&&this.canUsePlatformBroker(t)){o=new Yc(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,Os.acquireTokenRedirect,this.performanceClient,this.platformAuthProvider,this.getNativeAccountId(t),this.nativeInternalStorage,r).acquireTokenRedirect(t,n).catch((e=>{if(e instanceof Gc&&Qc(e)){this.platformAuthProvider=void 0;return this.createRedirectClient(r).acquireToken(t)}if(e instanceof Qn){this.logger.verbose("1ipyz4",r);return this.createRedirectClient(r).acquireToken(t)}throw e}))}else{o=this.createRedirectClient(r).acquireToken(t)}return await o}catch(o){throw this.browserStorage.resetRequestCache(r),2===n.event.status?this.performanceClient.startMeasurement(Va,r).end({success:!1},o,t.account):n.end({success:!1},o,t.account),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Redirect,null,o),o}}acquireTokenPopup(t){const r=this.getRequestCorrelationId(t),n=this.performanceClient.startMeasurement(Ga,r);n.add({scenarioId:t.scenarioId});try{this.logger.verbose("0ch87b",r),xh(this.initialized,n,this.config,t),this.browserStorage.setInteractionInProgress(!0,vs,t.overrideInteractionInProgress,r)}catch(e){return Promise.reject(e)}const o=this.getAllAccounts();let i;this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,r,e.InteractionType.Popup,t);const s=this.getPreGeneratedPkceCodes(r);if(this.canUsePlatformBroker(t))i=this.acquireTokenNative({...t,correlationId:r},Os.acquireTokenPopup).then((e=>(n.end({success:!0,isNativeBroker:!0},void 0,e.account),e))).catch((e=>{if(e instanceof Gc&&Qc(e)){this.platformAuthProvider=void 0;return this.createPopupClient(r).acquireToken(t,s)}if(e instanceof Qn){this.logger.verbose("0yy5fw",r);return this.createPopupClient(r).acquireToken(t,s)}throw e}));else{i=this.createPopupClient(r).acquireToken(t,s)}return i.then((t=>{const i=o.length(this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Popup,null,o),n.end({success:!1},o,t.account),Promise.reject(o)))).finally((async()=>{this.browserStorage.setInteractionInProgress(!1),this.config.system.navigatePopups||await this.preGeneratePkceCodes(r)}))}trackStateChangeWithMeasurement(e){const t=this.ssoSilentMeasurement||this.acquireTokenByCodeAsyncMeasurement;t&&("visibilitychange"===e.type?(this.logger.info("0yzimq",t.event.correlationId),t.increment({visibilityChangeCount:1})):"online"===e.type?(this.logger.info("1caf53",t.event.correlationId),t.increment({onlineStatusChangeCount:1})):"offline"===e.type&&(this.logger.info("0fdyk7",t.event.correlationId),t.increment({onlineStatusChangeCount:1})))}addStateChangeListeners(e){document.addEventListener("visibilitychange",e),window.addEventListener("online",e),window.addEventListener("offline",e)}removeStateChangeListeners(e){document.removeEventListener("visibilitychange",e),window.removeEventListener("online",e),window.removeEventListener("offline",e)}async ssoSilent(t){const r=this.getRequestCorrelationId(t),n={...t,prompt:t.prompt,correlationId:r};this.ssoSilentMeasurement=this.performanceClient.startMeasurement(Xa,r),this.ssoSilentMeasurement?.add({scenarioId:t.scenarioId}),xh(this.initialized,this.ssoSilentMeasurement,this.config,n),this.ssoSilentMeasurement?.increment({visibilityChangeCount:0,onlineStatusChangeCount:0}),this.addStateChangeListeners(this.trackStateChangeWithMeasurement);const o=this.getAllAccounts();let i;if(this.logger.verbose("0w1b45",r),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,r,e.InteractionType.Silent,n),this.canUsePlatformBroker(n))i=this.acquireTokenNative(n,Os.ssoSilent).catch((e=>{if(e instanceof Gc&&Qc(e)){this.platformAuthProvider=void 0;return this.createSilentIframeClient(n.correlationId).acquireToken(n)}throw e}));else{i=this.createSilentIframeClient(n.correlationId).acquireToken(n)}return i.then((t=>{const n=o.length{throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Silent,null,n),this.ssoSilentMeasurement?.end({success:!1},n,t.account),n})).finally((()=>{this.removeStateChangeListeners(this.trackStateChangeWithMeasurement)}))}async acquireTokenByCode(t){const r=this.getRequestCorrelationId(t);this.logger.trace("0ch6ga",r);const n=this.performanceClient.startMeasurement(Wa,r);xh(this.initialized,n,this.config,t),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,r,e.InteractionType.Silent,t),n.add({scenarioId:t.scenarioId});try{if(t.code&&t.nativeAccountId)throw es(Ui);if(t.code){const o=t.code;let i=this.hybridAuthCodeResponses.get(o);return i?(this.logger.verbose("0qgp28",r),n.discard()):(this.logger.verbose("06eh73",r),i=this.acquireTokenByCodeAsync({...t,correlationId:r}).then((t=>(this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,r,e.InteractionType.Silent,t),this.hybridAuthCodeResponses.delete(o),n.end({success:!0,isNativeBroker:t.fromPlatformBroker,accessTokenSize:t.accessToken.length,idTokenSize:t.idToken.length},void 0,t.account),t))).catch((t=>{throw this.hybridAuthCodeResponses.delete(o),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Silent,null,t),n.end({success:!1},t),t})),this.hybridAuthCodeResponses.set(o,i)),await i}if(t.nativeAccountId){if(this.canUsePlatformBroker(t,t.nativeAccountId)){const e=await this.acquireTokenNative({...t,correlationId:r},Os.acquireTokenByCode,t.nativeAccountId).catch((e=>{throw e instanceof Gc&&Qc(e)&&(this.platformAuthProvider=void 0),e}));return n.end({success:!0},void 0,e.account),e}throw es(Hi)}throw es(Ni)}catch(t){throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Silent,null,t),n.end({success:!1},t),t}}async acquireTokenByCodeAsync(e){const t=this.getRequestCorrelationId(e);this.logger.trace("10d9hy",t),this.acquireTokenByCodeAsyncMeasurement=this.performanceClient.startMeasurement("acquireTokenByCodeAsync",t),this.acquireTokenByCodeAsyncMeasurement?.increment({visibilityChangeCount:0,onlineStatusChangeCount:0}),this.addStateChangeListeners(this.trackStateChangeWithMeasurement);const r=this.createSilentAuthCodeClient(t);return await r.acquireToken(e).then((e=>(this.acquireTokenByCodeAsyncMeasurement?.end({success:!0,fromCache:e.fromCache,isNativeBroker:e.fromPlatformBroker}),e))).catch((e=>{throw this.acquireTokenByCodeAsyncMeasurement?.end({success:!1},e),e})).finally((()=>{this.removeStateChangeListeners(this.trackStateChangeWithMeasurement)}))}async acquireTokenFromCache(e,t){switch(t){case Hs.Default:case Hs.AccessToken:case Hs.AccessTokenAndRefreshToken:const t=this.createSilentCacheClient(e.correlationId);return qn(t.acquireToken.bind(t),"silentCacheClientAcquireToken",this.logger,this.performanceClient,e.correlationId)(e);default:throw be(ft)}}async acquireTokenByRefreshToken(e,t){switch(t){case Hs.Default:case Hs.AccessTokenAndRefreshToken:case Hs.RefreshToken:case Hs.RefreshTokenAndNetwork:const t=this.createSilentRefreshClient(e.correlationId);return qn(t.acquireToken.bind(t),"silentRefreshClientAcquireToken",this.logger,this.performanceClient,e.correlationId)(e);default:throw be(ft)}}async acquireTokenBySilentIframe(e){const t=this.createSilentIframeClient(e.correlationId);return qn(t.acquireToken.bind(t),"silentIframeClientAcquireToken",this.logger,this.performanceClient,e.correlationId)(e)}async logoutRedirect(e){const t=this.getRequestCorrelationId(e);Ua(this.initialized,this.config),this.browserStorage.setInteractionInProgress(!0,ks);return this.createRedirectClient(t).logout(e)}logoutPopup(e){try{const t=this.getRequestCorrelationId(e);Na(this.initialized),this.browserStorage.setInteractionInProgress(!0,ks);return this.createPopupClient(t).logout(e).finally((()=>{this.browserStorage.setInteractionInProgress(!1)}))}catch(e){return Promise.reject(e)}}async clearCache(e){if(!this.isBrowserEnvironment)return;const t=this.getRequestCorrelationId(e);return this.createSilentCacheClient(t).logout(e)}getAllAccounts(e){return Rc(this.logger,this.browserStorage,this.isBrowserEnvironment,this.getRequestCorrelationId(),e)}getAccount(e){return Oc(e,this.logger,this.browserStorage,this.getRequestCorrelationId())}setActiveAccount(e){Mc(e,this.browserStorage,this.getRequestCorrelationId())}getActiveAccount(){return xc(this.browserStorage,this.getRequestCorrelationId())}async hydrateCache(e,t){this.logger.verbose("16jycr",e.correlationId);const r=zr(e.account,e.cloudGraphHostName,e.msGraphHost);return await this.browserStorage.setAccount(r,e.correlationId,kr(e.idTokenClaims),Os.hydrateCache),e.fromPlatformBroker?(this.logger.verbose("1fxyu8",e.correlationId),this.nativeInternalStorage.hydrateCache(e,t)):this.browserStorage.hydrateCache(e,t)}async acquireTokenNative(e,t,r,n){const o=this.getRequestCorrelationId(e);if(this.logger.trace("0b9y3p",o),!this.platformAuthProvider)throw es(Ki);return new Yc(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,t,this.performanceClient,this.platformAuthProvider,r||this.getNativeAccountId(e),this.nativeInternalStorage,o).acquireToken(e,n)}canUsePlatformBroker(e,t){const r=this.getRequestCorrelationId(e);if(this.logger.trace("1n9lbl",r),!this.platformAuthProvider)return this.logger.trace("0vnu11",r),!1;if(!Ch(this.config,this.logger,r,this.platformAuthProvider,e.authenticationScheme))return this.logger.trace("1m4bzf",r),!1;if(e.prompt)switch(e.prompt){case R.NONE:case R.CONSENT:case R.LOGIN:this.logger.trace("0vdv8e",r);break;default:return this.logger.trace("0pdzw6",r),!1}return!(!t&&!this.getNativeAccountId(e))||(this.logger.trace("16lbtk",r),!1)}getNativeAccountId(e){const t=e.account||this.getAccount({loginHint:e.loginHint,sid:e.sid})||this.getActiveAccount();return t&&t.nativeAccountId||""}createPopupClient(e){return new vh(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,this.performanceClient,this.nativeInternalStorage,e,this.platformAuthProvider)}createRedirectClient(e){return new kh(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,this.performanceClient,this.nativeInternalStorage,e,this.platformAuthProvider)}createSilentIframeClient(e){return new Ph(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,Os.ssoSilent,this.performanceClient,this.nativeInternalStorage,e,this.platformAuthProvider)}createSilentCacheClient(e){return new Xc(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,this.performanceClient,e,this.platformAuthProvider)}createSilentRefreshClient(e){return new Rh(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,this.performanceClient,e,this.platformAuthProvider)}createSilentAuthCodeClient(e){return new Mh(this.config,this.browserStorage,this.browserCrypto,this.logger,this.eventHandler,this.navigationClient,Os.acquireTokenByCode,this.performanceClient,e,this.platformAuthProvider)}addEventCallback(e,t){return this.eventHandler.addEventCallback(e,t)}removeEventCallback(e){this.eventHandler.removeEventCallback(e)}addPerformanceCallback(e){return xa(),this.performanceClient.addPerformanceCallback(e)}removePerformanceCallback(e){return this.performanceClient.removePerformanceCallback(e)}getLogger(){return this.logger}setLogger(e){this.logger=e}initializeWrapperLibrary(e,t){this.browserStorage.setWrapperMetadata(e,t)}setNavigationClient(e){this.navigationClient=e}getConfiguration(){return this.config}getPerformanceClient(){return this.performanceClient}isBrowserEnv(){return this.isBrowserEnvironment}getRequestCorrelationId(e){return e?.correlationId?e.correlationId:this.isBrowserEnvironment?ia():""}async loginRedirect(e){const t=this.getRequestCorrelationId(e);return this.logger.verbose("0lz9hf",t),this.acquireTokenRedirect({correlationId:t,...e||Ns})}loginPopup(e){const t=this.getRequestCorrelationId(e);return this.logger.verbose("0qw7v5",t),this.acquireTokenPopup({correlationId:t,...e||Ns})}async acquireTokenSilent(e){const t=this.getRequestCorrelationId(e),r=this.performanceClient.startMeasurement(Ja,t);r.add({cacheLookupPolicy:e.cacheLookupPolicy,scenarioId:e.scenarioId}),xh(this.initialized,r,this.config,e),this.logger.verbose("0x1c4s",t);const n=e.account||this.getActiveAccount();if(!n)throw es(Ti);return this.acquireTokenSilentDeduped(e,n,t).then((n=>(r.end({success:!0,fromCache:n.fromCache,isNativeBroker:n.fromPlatformBroker,accessTokenSize:n.accessToken.length,idTokenSize:n.idToken.length},void 0,n.account),{...n,state:e.state,correlationId:t}))).catch((e=>{throw e instanceof we&&e.setCorrelationId(t),r.end({success:!1},e,n),e}))}async acquireTokenSilentDeduped(e,t,r){const n=so(this.config.auth.clientId,{...e,authority:e.authority||this.config.auth.authority},t.homeAccountId),o=JSON.stringify(n),i=this.activeSilentTokenRequests.get(o);if(void 0===i){this.logger.verbose("0fcjbk",r),this.performanceClient.addFields({deduped:!1},r);const n=qn(this.acquireTokenSilentAsync.bind(this),"acquireTokenSilentAsync",this.logger,this.performanceClient,r)({...e,correlationId:r},t);return this.activeSilentTokenRequests.set(o,n),n.finally((()=>{this.activeSilentTokenRequests.delete(o)}))}return this.logger.verbose("1yq7nb",r),this.performanceClient.addFields({deduped:!0},r),i}async acquireTokenSilentAsync(t,r){const n=e=>this.trackStateChange(t.correlationId,e);this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,t.correlationId,e.InteractionType.Silent,t),t.correlationId&&this.performanceClient.incrementFields({visibilityChangeCount:0,onlineStatusChangeCount:0},t.correlationId),this.addStateChangeListeners(n);const o=await qn(Kc,"initializeSilentRequest",this.logger,this.performanceClient,t.correlationId)(t,r,this.config,this.performanceClient,this.logger),i=t.cacheLookupPolicy||Hs.Default;return this.acquireTokenSilentNoIframe(o,i).catch((async e=>{const r=function(e,t){const r=!(e instanceof Qn&&e.subError!==jn),n=e.errorCode===ts||e.errorCode===ft,o=r&&n||e.errorCode===Ln||e.errorCode===Dn,i=Ds.includes(t);return o&&i}(e,i);if(r){const r=`${e.errorCode}${e.subError?`|${e.subError}`:""}`;if(this.performanceClient.addFields({silentRefreshReason:r},t.correlationId),this.activeIframeRequest){if(i!==Hs.Skip){const[t,r]=this.activeIframeRequest;this.logger.verbose("1w8fso",o.correlationId);const n=this.performanceClient.startMeasurement("awaitConcurrentIframe",o.correlationId);n.add({awaitIframeCorrelationId:r});const s=await t;if(n.end({success:s}),s)return this.logger.verbose("0ywzzi",o.correlationId),this.acquireTokenSilentNoIframe(o,i);throw this.logger.info("17y14q",o.correlationId),e}return this.logger.warning("1bd4p8",o.correlationId),qn(this.acquireTokenBySilentIframe.bind(this),Fo,this.logger,this.performanceClient,o.correlationId)(o)}{let e;return this.activeIframeRequest=[new Promise((t=>{e=t})),o.correlationId],this.logger.verbose("0rh08z",o.correlationId),qn(this.acquireTokenBySilentIframe.bind(this),Fo,this.logger,this.performanceClient,o.correlationId)(o).then((t=>(e(!0),t))).catch((t=>{throw e(!1),t})).finally((()=>{this.activeIframeRequest=void 0}))}}throw e})).then((r=>(this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,t.correlationId,e.InteractionType.Silent,r),t.correlationId&&this.performanceClient.addFields({fromCache:r.fromCache,isNativeBroker:r.fromPlatformBroker},t.correlationId),r))).catch((r=>{throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,t.correlationId,e.InteractionType.Silent,null,r),r})).finally((()=>{this.removeStateChangeListeners(n)}))}async acquireTokenSilentNoIframe(t,r){return Ch(this.config,this.logger,t.correlationId,this.platformAuthProvider,t.authenticationScheme)&&t.account.nativeAccountId?(this.logger.verbose("0sczo4",t.correlationId),this.acquireTokenNative(t,Os.acquireTokenSilent_silentFlow,t.account.nativeAccountId,r).catch((async e=>{if(e instanceof Gc&&Qc(e))throw this.logger.verbose("07rkmb",t.correlationId),this.platformAuthProvider=void 0,be(ft);throw e}))):(this.logger.verbose("0ox81t",t.correlationId),r===Hs.AccessToken&&this.logger.verbose("0fvwxe",t.correlationId),qn(this.acquireTokenFromCache.bind(this),"acquireTokenFromCache",this.logger,this.performanceClient,t.correlationId)(t,r).catch((n=>{if(r===Hs.AccessToken)throw n;return this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_NETWORK_START,t.correlationId,e.InteractionType.Silent,t),qn(this.acquireTokenByRefreshToken.bind(this),"acquireTokenByRefreshToken",this.logger,this.performanceClient,t.correlationId)(t,r)})))}async preGeneratePkceCodes(e){return this.logger.verbose("1x6uj6",e),this.pkceCode=await qn(hh,Zo,this.logger,this.performanceClient,e)(this.performanceClient,this.logger,e),Promise.resolve()}getPreGeneratedPkceCodes(e){const t=this.pkceCode?{...this.pkceCode}:void 0;return this.pkceCode=void 0,t?this.logger.verbose("12js1o",e):this.logger.verbose("1oe9ci",e),this.performanceClient.addFields({usePreGeneratedPkce:!!t},e),t}logMultipleInstances(e,t){const r=this.config.auth.clientId;if(!window)return;window.msal=window.msal||{},window.msal.clientIds=window.msal.clientIds||[];window.msal.clientIds.length>0&&this.logger.verbose("1qtz3l",t),window.msal.clientIds.push(r),function(e,t,r,n){const o=window.msal?.clientIds||[],i=o.length,s=o.filter((t=>t===e)).length;s>1&&r.warning("1e88vg",n),t.add({msalInstanceCount:i,sameClientIdInstanceCount:s})}(r,e,this.logger,t)}}class Nh{static loggerCallback(t,r){switch(t){case e.LogLevel.Error:return void console.error(r);case e.LogLevel.Info:return void console.info(r);case e.LogLevel.Verbose:return void console.debug(r);case e.LogLevel.Warning:return void console.warn(r);default:return void console.log(r)}}constructor(t){let r;this.browserEnvironment="undefined"!=typeof window,this.config=fh(t,this.browserEnvironment);try{r=window[ws.SessionStorage]}catch(e){}const n=r?.getItem(oc),o=r?.getItem(ic)?.toLowerCase(),i="true"===o||"false"!==o&&void 0,s={...this.config.system.loggerOptions},a=n&&Object.keys(e.LogLevel).includes(n)?e.LogLevel[n]:void 0;a&&(s.loggerCallback=Nh.loggerCallback,s.logLevel=a),void 0!==i&&(s.piiLoggingEnabled=i),this.logger=new pr(s,bc,Ac),this.available=!1}getConfig(){return this.config}getLogger(){return this.logger}isAvailable(){return this.available}isBrowserEnvironment(){return this.browserEnvironment}}class Uh extends Nh{getModuleName(){return Uh.MODULE_NAME}getId(){return Uh.ID}async initialize(e){return this.available="undefined"!=typeof window,this.available}}Uh.MODULE_NAME="",Uh.ID="StandardOperatingContext";const Lh="USER_INTERACTION_REQUIRED",Hh="USER_CANCEL",Dh="NO_NETWORK",Fh="TRANSIENT_ERROR",Kh="PERSISTENT_ERROR",Bh="DISABLED",zh="ACCOUNT_UNAVAILABLE",jh="NESTED_APP_AUTH_UNAVAILABLE";class $h{constructor(e,t,r,n){this.clientId=e,this.clientCapabilities=t,this.crypto=r,this.logger=n}toNaaTokenRequest(e){let t;t=void 0===e.extraQueryParameters?new Map:new Map(Object.entries(e.extraQueryParameters));const r=e.correlationId||this.crypto.createNewGuid(),n=Yt(e.claims,this.clientCapabilities),o=e.scopes||p;return{platformBrokerId:e.account?.homeAccountId,clientId:this.clientId,authority:e.authority,resource:e.resource,scope:o.join(" "),correlationId:r,claims:ke.isEmptyObj(n)?void 0:n,state:e.state,authenticationScheme:e.authenticationScheme||G.BEARER,extraParameters:t}}fromNaaTokenResponse(e,t,r){if(!t.token.id_token||!t.token.access_token)throw be(Qe);const n=dn(r+(t.token.expires_in||0)),o=vr(t.token.id_token,this.crypto.base64Decode),i=this.fromNaaAccountInfo(t.account,t.token.id_token,o),s=t.token.scope||e.scope;return{authority:t.token.authority||i.environment,uniqueId:i.localAccountId,tenantId:i.tenantId,scopes:s.split(" "),account:i,idToken:t.token.id_token,idTokenClaims:o,accessToken:t.token.access_token,fromCache:!1,expiresOn:n,tokenType:e.authenticationScheme||G.BEARER,correlationId:e.correlationId,extExpiresOn:n,state:e.state}}fromNaaAccountInfo(e,t,r){const n=r||e.idTokenClaims,o=e.localAccountId||n?.oid||n?.sub||"",i=e.tenantId||Fr(n)||"",s=e.homeAccountId||`${o}.${i}`,a=e.environment;if(!a)throw be(gt);const c=n?.preferred_username||n?.upn,h=n?.emails?.[0]||null,l=e.username||c||h||"",d=e.name||n?.name||"",u=e.loginHint||n?.login_hint,g=new Map,p=Ir(s,o,i,n);g.set(i,p);return{homeAccountId:s,environment:a,tenantId:i,username:l,localAccountId:o,name:d,loginHint:u,idToken:t,idTokenClaims:n,tenantProfiles:g}}fromBridgeError(e){if(!function(e){return void 0!==e.status}(e))return new we("unknown_error","An unknown error occurred");switch(e.status){case Hh:return new Te(Tt);case Dh:return new Te(kt);case zh:return new Te(pt);case Bh:return new Te(At);case jh:return new Te(e.code||At,e.description);case Fh:case Kh:return new Yn(e.code,e.description);case Lh:return new Qn(e.code,e.description);default:return new we(e.code,e.description)}}toAuthenticationResultFromCache(e,t,r,n,o){if(!t||!r)throw be(Qe);const i=vr(t.secret,this.crypto.base64Decode),s=r.target||n.scopes.join(" ");return{authority:r.environment||e.environment,uniqueId:e.localAccountId,tenantId:e.tenantId,scopes:s.split(" "),account:e,idToken:t.secret,idTokenClaims:i||{},accessToken:r.secret,fromCache:!0,expiresOn:dn(r.expiresOn),extExpiresOn:dn(r.extendedExpiresOn),tokenType:n.authenticationScheme||G.BEARER,correlationId:o,state:n.state}}}class Jh extends we{constructor(e,t){super(e,t),Object.setPrototypeOf(this,Jh.prototype),this.name="NestedAppAuthError"}static createUnsupportedError(){return new Jh("unsupported_method")}}class Wh{constructor(e){this.operatingContext=e;const t=this.operatingContext.getBridgeProxy();if(void 0===t)throw new Error("unexpected: bridgeProxy is undefined");this.bridgeProxy=t,this.config=e.getConfig(),this.logger=this.operatingContext.getLogger(),this.performanceClient=this.config.telemetry.client,this.browserCrypto=e.isBrowserEnvironment()?new ja(this.logger,this.performanceClient,!0):lr,this.eventHandler=new qc(this.logger),this.browserStorage=this.operatingContext.isBrowserEnvironment()?new _c(this.config.auth.clientId,this.config.cache,this.browserCrypto,this.logger,this.performanceClient,this.eventHandler,yo(this.config.auth)):Pc(this.config.auth.clientId,this.logger,this.performanceClient,this.eventHandler),this.nestedAppAuthAdapter=new $h(this.config.auth.clientId,this.config.auth.clientCapabilities,this.browserCrypto,this.logger);const r=this.bridgeProxy.getAccountContext();this.currentAccountContext=r||null}static async createController(e){const t=new Wh(e);return Promise.resolve(t)}async initialize(e,t){const r=e?.correlationId||ia();return await this.browserStorage.initialize(r),Promise.resolve()}ensureValidRequest(e){return e?.correlationId?e:{...e,correlationId:this.browserCrypto.createNewGuid()}}async acquireTokenInteractive(t){const r=this.ensureValidRequest(t),n=r.correlationId||ia();this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,n,e.InteractionType.Popup,r);const o=this.performanceClient.startMeasurement(Ga,n);o.add({nestedAppAuthRequest:!0});try{So(this.config.auth.isMcp,r);const i=this.nestedAppAuthAdapter.toNaaTokenRequest(r),s=hn(),a=await this.bridgeProxy.getTokenInteractive(i),c={...this.nestedAppAuthAdapter.fromNaaTokenResponse(i,a,s)};try{await this.hydrateCache(c,t)}catch(e){this.logger.warningPii("1mwr91",n)}return this.currentAccountContext={homeAccountId:c.account.homeAccountId,environment:c.account.environment,tenantId:c.account.tenantId},this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,n,e.InteractionType.Popup,c),o.add({accessTokenSize:c.accessToken.length,idTokenSize:c.idToken.length}),o.end({success:!0,requestId:c.requestId},void 0,c.account),c}catch(r){const i=r instanceof we?r:this.nestedAppAuthAdapter.fromBridgeError(r);throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,n,e.InteractionType.Popup,null,r),o.end({success:!1},r,t.account),i}}async acquireTokenSilentInternal(t){const r=this.ensureValidRequest(t),n=r.correlationId||ia();this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_START,n,e.InteractionType.Silent,r);const o=await this.acquireTokenFromCache(r);if(o)return this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,n,e.InteractionType.Silent,o),o;const i=this.performanceClient.startMeasurement(Xa,n);i.increment({visibilityChangeCount:0}),i.add({nestedAppAuthRequest:!0});try{So(this.config.auth.isMcp,r);const o=this.nestedAppAuthAdapter.toNaaTokenRequest(r);o.forceRefresh=r.forceRefresh;const s=hn(),a=await this.bridgeProxy.getTokenSilent(o),c=this.nestedAppAuthAdapter.fromNaaTokenResponse(o,a,s);try{await this.hydrateCache(c,t)}catch(e){this.logger.warningPii("1mwr91",n)}return this.currentAccountContext={homeAccountId:c.account.homeAccountId,environment:c.account.environment,tenantId:c.account.tenantId},this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,n,e.InteractionType.Silent,c),i?.add({accessTokenSize:c.accessToken.length,idTokenSize:c.idToken.length}),i?.end({success:!0,requestId:c.requestId},void 0,c.account),c}catch(r){const o=r instanceof we?r:this.nestedAppAuthAdapter.fromBridgeError(r);throw this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,n,e.InteractionType.Silent,null,r),i?.end({success:!1},r,t.account),o}}async acquireTokenFromCache(t){const r=t.correlationId||ia(),n=this.performanceClient.startMeasurement(Ja,r);if(n?.add({nestedAppAuthRequest:!0}),t.claims)return this.logger.verbose("11t57w",r),null;if(t.forceRefresh)return this.logger.verbose("1ovnmo",r),null;let o=null;switch(t.cacheLookupPolicy||(t.cacheLookupPolicy=Hs.Default),t.cacheLookupPolicy){case Hs.Default:case Hs.AccessToken:case Hs.AccessTokenAndRefreshToken:o=await this.acquireTokenFromCacheInternal(t);break;default:return null}return o?(this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_SUCCESS,r,e.InteractionType.Silent,o),n.add({accessTokenSize:o.accessToken.length,idTokenSize:o.idToken.length}),n.end({success:!0},void 0,o.account),o):(this.logger.warning("1yb4fi",r),this.eventHandler.emitEvent(Tc.ACQUIRE_TOKEN_FAILURE,r,e.InteractionType.Silent,null),n.end({success:!1},void 0,t.account),null)}async acquireTokenFromCacheInternal(e){const t=this.bridgeProxy.getAccountContext()||this.currentAccountContext,r=e.correlationId||ia();let n=null;if(t&&(n=Oc(t,this.logger,this.browserStorage,r)),!n)return this.logger.verbose("10qnr0",r),Promise.resolve(null);this.logger.verbose("1u7hux",r);const o={...e,correlationId:r,authority:e.authority||n.environment,scopes:e.scopes?.length?e.scopes:[...p]},i=this.browserStorage.getTokenKeys(),s=this.browserStorage.getAccessToken(n,o,i,n.tenantId);if(!s)return this.logger.verbose("03vm49",r),Promise.resolve(null);if(pn(s.cachedAt)||un(s.expiresOn,this.config.system.tokenRenewalOffsetSeconds))return this.logger.verbose("18egye",r),Promise.resolve(null);if(o.resource){const e=o.resource,t=s.resource;if(!t||t!==e)return this.logger.verbose("0qraxd",r),Promise.resolve(null)}const a=this.browserStorage.getIdToken(n,o.correlationId,i,n.tenantId);return a?this.nestedAppAuthAdapter.toAuthenticationResultFromCache(n,a,s,o,o.correlationId):(this.logger.verbose("0d68kd",r),Promise.resolve(null))}async acquireTokenPopup(e){return this.acquireTokenInteractive(e)}acquireTokenRedirect(e){throw Jh.createUnsupportedError()}async acquireTokenSilent(e){return this.acquireTokenSilentInternal(e)}acquireTokenByCode(e){throw Jh.createUnsupportedError()}addEventCallback(e,t){return this.eventHandler.addEventCallback(e,t)}removeEventCallback(e){this.eventHandler.removeEventCallback(e)}addPerformanceCallback(e){throw Jh.createUnsupportedError()}removePerformanceCallback(e){throw Jh.createUnsupportedError()}getAllAccounts(e){return Rc(this.logger,this.browserStorage,this.isBrowserEnv(),ia(),e)}getAccount(e){return Oc(e,this.logger,this.browserStorage,ia())}setActiveAccount(e){return Mc(e,this.browserStorage,ia())}getActiveAccount(){return xc(this.browserStorage,ia())}handleRedirectPromise(e){return Promise.resolve(null)}loginPopup(e){return this.acquireTokenInteractive(e||Ns)}loginRedirect(e){throw Jh.createUnsupportedError()}logoutRedirect(e){throw Jh.createUnsupportedError()}logoutPopup(e){throw Jh.createUnsupportedError()}ssoSilent(e){return this.acquireTokenSilentInternal(e)}getLogger(){return this.logger}setLogger(e){this.logger=e}initializeWrapperLibrary(e,t){}setNavigationClient(e){this.logger.warning("1k8729","")}getConfiguration(){return this.config}isBrowserEnv(){return this.operatingContext.isBrowserEnvironment()}getBrowserCrypto(){return this.browserCrypto}getPerformanceClient(){throw Jh.createUnsupportedError()}getRedirectResponse(){throw Jh.createUnsupportedError()}async clearCache(e){throw Jh.createUnsupportedError()}async hydrateCache(e,t){this.logger.verbose("16jycr",e.correlationId);const r=zr(e.account,e.cloudGraphHostName,e.msGraphHost);return await this.browserStorage.setAccount(r,e.correlationId,kr(e.idTokenClaims),Os.hydrateCache),this.browserStorage.hydrateCache(e,t)}}class Gh{static async initializeNestedAppAuthBridge(){if(void 0===window)throw new Error("window is undefined");if(void 0===window.nestedAppAuthBridge)throw new Error("window.nestedAppAuthBridge is undefined");try{window.nestedAppAuthBridge.addEventListener("message",(e=>{const t="string"==typeof e?e:e.data,r=JSON.parse(t),n=Gh.bridgeRequests.find((e=>e.requestId===r.requestId));void 0!==n&&(Gh.bridgeRequests.splice(Gh.bridgeRequests.indexOf(n),1),r.success?n.resolve(r):n.reject(r.error))}));const e=await new Promise(((e,t)=>{const r=Gh.buildRequest("GetInitContext"),n={requestId:r.requestId,method:r.method,resolve:e,reject:t};Gh.bridgeRequests.push(n),window.nestedAppAuthBridge.postMessage(JSON.stringify(r))}));return Gh.validateBridgeResultOrThrow(e.initContext)}catch(e){throw window.console.log(e),e}}getTokenInteractive(e){return this.getToken("GetTokenPopup",e)}getTokenSilent(e){return this.getToken("GetToken",e)}async getToken(e,t){const r=await this.sendRequest(e,{tokenParams:t});return{token:Gh.validateBridgeResultOrThrow(r.token),account:Gh.validateBridgeResultOrThrow(r.account)}}getHostCapabilities(){return this.capabilities??null}getAccountContext(){return this.accountContext?this.accountContext:null}static buildRequest(e,t){return{messageType:"NestedAppAuthRequest",method:e,requestId:ia(),sendTime:Date.now(),clientLibrary:is,clientLibraryVersion:Ac,...t}}sendRequest(e,t){const r=Gh.buildRequest(e,t);return new Promise(((e,t)=>{const n={requestId:r.requestId,method:r.method,resolve:e,reject:t};Gh.bridgeRequests.push(n),window.nestedAppAuthBridge.postMessage(JSON.stringify(r))}))}static validateBridgeResultOrThrow(e){if(void 0===e){throw{status:jh}}return e}constructor(e,t,r,n){this.sdkName=e,this.sdkVersion=t,this.accountContext=r,this.capabilities=n}static async create(){const e=await Gh.initializeNestedAppAuthBridge();return new Gh(e.sdkName,e.sdkVersion,e.accountContext,e.capabilities)}}Gh.bridgeRequests=[];class Qh extends Nh{constructor(){super(...arguments),this.bridgeProxy=void 0,this.accountContext=null}getModuleName(){return Qh.MODULE_NAME}getId(){return Qh.ID}getBridgeProxy(){return this.bridgeProxy}async initialize(e){const t=e||"";try{if("undefined"!=typeof window){"function"==typeof window.__initializeNestedAppAuth&&await window.__initializeNestedAppAuth();const e=await Gh.create();this.accountContext=e.getAccountContext(),this.bridgeProxy=e,this.available=void 0!==e}}catch(e){this.logger.infoPii("1mdxyj",t)}return this.logger.info("12jy9a",t),this.available}}Qh.MODULE_NAME="",Qh.ID="NestedAppOperatingContext";class Vh{constructor(e,t){this.controller=t||new qh(new Uh(e))}async initialize(e){return this.controller.initialize(e)}async acquireTokenPopup(e){return this.controller.acquireTokenPopup(e)}acquireTokenRedirect(e){return this.controller.acquireTokenRedirect(e)}acquireTokenSilent(e){return this.controller.acquireTokenSilent(e)}acquireTokenByCode(e){return this.controller.acquireTokenByCode(e)}addEventCallback(e,t){return this.controller.addEventCallback(e,t)}removeEventCallback(e){return this.controller.removeEventCallback(e)}addPerformanceCallback(e){return this.controller.addPerformanceCallback(e)}removePerformanceCallback(e){return this.controller.removePerformanceCallback(e)}getAccount(e){return this.controller.getAccount(e)}getAllAccounts(e){return this.controller.getAllAccounts(e)}handleRedirectPromise(e){return this.controller.handleRedirectPromise(e)}loginPopup(e){return this.controller.loginPopup(e)}loginRedirect(e){return this.controller.loginRedirect(e)}logoutRedirect(e){return this.controller.logoutRedirect(e)}logoutPopup(e){return this.controller.logoutPopup(e)}ssoSilent(e){return this.controller.ssoSilent(e)}getLogger(){return this.controller.getLogger()}setLogger(e){this.controller.setLogger(e)}setActiveAccount(e){this.controller.setActiveAccount(e)}getActiveAccount(){return this.controller.getActiveAccount()}initializeWrapperLibrary(e,t){return this.controller.initializeWrapperLibrary(e,t)}setNavigationClient(e){this.controller.setNavigationClient(e)}getConfiguration(){return this.controller.getConfiguration()}async hydrateCache(e,t){return this.controller.hydrateCache(e,t)}clearCache(e){return this.controller.clearCache(e)}}async function Xh(e){const t=new Vh(e);return await t.initialize(),t}const Yh={initialize:()=>Promise.reject(Ia(ma)),acquireTokenPopup:()=>Promise.reject(Ia(ma)),acquireTokenRedirect:()=>Promise.reject(Ia(ma)),acquireTokenSilent:()=>Promise.reject(Ia(ma)),acquireTokenByCode:()=>Promise.reject(Ia(ma)),getAllAccounts:()=>[],getAccount:()=>null,handleRedirectPromise:()=>Promise.reject(Ia(ma)),loginPopup:()=>Promise.reject(Ia(ma)),loginRedirect:()=>Promise.reject(Ia(ma)),logoutRedirect:()=>Promise.reject(Ia(ma)),logoutPopup:()=>Promise.reject(Ia(ma)),ssoSilent:()=>Promise.reject(Ia(ma)),addEventCallback:()=>null,removeEventCallback:()=>{},addPerformanceCallback:()=>"",removePerformanceCallback:()=>!1,getLogger:()=>{throw Ia(ma)},setLogger:()=>{},setActiveAccount:()=>{},getActiveAccount:()=>null,initializeWrapperLibrary:()=>{},setNavigationClient:()=>{},getConfiguration:()=>{throw Ia(ma)},hydrateCache:()=>Promise.reject(Ia(ma)),clearCache:()=>Promise.reject(Ia(ma))};async function Zh(e,t,r,n,o,i,s,a,c){if(o.verbose("0ke46k",r),e.account){const t=zr(e.account);return await n.setAccount(t,r,kr(a||{}),Os.loadExternalTokens),t}if(!t&&!a)throw o.error("0hzcn4",r),es(Mi);const h=jr(t,s.authorityType,o,i,r,a),l=a?.tid,d=ro(n,s,h,js,r,a,t,s.getPreferredCache(),l,void 0,void 0,o,c);return await n.setAccount(d,r,kr(a||{}),Os.loadExternalTokens),d}async function el(e,t,r,n,o,i,s,a,c){if(!e.id_token)return a.verbose("1pm7g1",i),null;a.verbose("168lyi",i);const h=mn(t,r,e.id_token,c,n);return await s.setIdTokenCredential(h,i,o),h}async function tl(e,t,r,n,o,i,s,a,c,h,l){if(!t.access_token)return h.verbose("1ckp9e",a),null;if(!t.expires_in)return h.error("15mzx8",a),null;if(!(t.scope||e.scopes&&e.scopes.length))return h.error("1h7xse",a),null;h.verbose("01kmxb",a);const d=t.scope?Pt.fromString(t.scope):new Pt(e.scopes),u=s.expiresOn||t.expires_in+hn(),g=s.extendedExpiresOn||(t.ext_expires_in||t.expires_in)+hn(),p=fn(r,n,t.access_token,l,o,d.printScopes(),u,g,js);return await c.setAccessTokenCredential(p,a,i),p}async function rl(e,t,r,n,o,i,s,a,c){if(!e.refresh_token)return s.verbose("1l7um5",o),null;const h=e.refresh_token_expires_in?e.refresh_token_expires_in+hn():void 0;c.addFields({extRtExpiresOnSeconds:h},o),s.verbose("0qy8ev",o);const l=yn(t,r,e.refresh_token,a,e.foci,void 0,h);return await i.setRefreshTokenCredential(l,o,n),l}function nl(){let e;try{e=window[ws.SessionStorage];const t=e?.getItem(sc);if(1===Number(t))return Promise.resolve().then((function(){return al}))}catch(e){}}function ol(){return"undefined"!=typeof window&&void 0!==window.performance&&"function"==typeof window.performance.now}function il(e){if(e&&ol())return Math.round(window.performance.now()-e)}class sl{constructor(e,t){this.correlationId=t,this.measureName=sl.makeMeasureName(e,t),this.startMark=sl.makeStartMark(e,t),this.endMark=sl.makeEndMark(e,t)}static makeMeasureName(e,t){return`msal.measure.${e}.${t}`}static makeStartMark(e,t){return`msal.start.${e}.${t}`}static makeEndMark(e,t){return`msal.end.${e}.${t}`}static supportsBrowserPerformance(){return"undefined"!=typeof window&&void 0!==window.performance&&"function"==typeof window.performance.mark&&"function"==typeof window.performance.measure&&"function"==typeof window.performance.clearMarks&&"function"==typeof window.performance.clearMeasures&&"function"==typeof window.performance.getEntriesByName}static flushMeasurements(e,t){if(sl.supportsBrowserPerformance())try{t.forEach((t=>{const r=sl.makeMeasureName(t.name,e);window.performance.getEntriesByName(r,"measure").length>0&&(window.performance.clearMeasures(r),window.performance.clearMarks(sl.makeStartMark(r,e)),window.performance.clearMarks(sl.makeEndMark(r,e)))}))}catch(e){}}startMeasurement(){if(sl.supportsBrowserPerformance())try{window.performance.mark(this.startMark)}catch(e){}}endMeasurement(){if(sl.supportsBrowserPerformance())try{window.performance.mark(this.endMark),window.performance.measure(this.measureName,this.startMark,this.endMark)}catch(e){}}flushMeasurement(){if(sl.supportsBrowserPerformance())try{const e=window.performance.getEntriesByName(this.measureName,"measure");if(e.length>0){const t=e[0].duration;return window.performance.clearMeasures(this.measureName),window.performance.clearMarks(this.startMark),window.performance.clearMarks(this.endMark),t}}catch(e){}return null}}var al=Object.freeze({__proto__:null,BrowserPerformanceMeasurement:sl});const cl=G,hl=x,ll=R,dl=ae,ul=p;e.ApiId=Os,e.AuthError=we,e.AuthErrorCodes=Po,e.AuthenticationHeaderParser=class{constructor(e){this.headers=e}getShrNonce(){const e=this.headers[v];if(e){const t=this.parseChallenges(e);if(t.nextnonce)return t.nextnonce;throw ve(Fe)}const t=this.headers[C];if(t){const e=this.parseChallenges(t);if(e.nonce)return e.nonce;throw ve(Fe)}throw ve(De)}parseChallenges(e){const t=e.indexOf(" "),r=e.substr(t+1).split(","),n={};return r.forEach((e=>{const[t,r]=e.split("=");n[t]=unescape(r.replace(/['"]+/g,""))})),n}},e.AuthenticationScheme=cl,e.AzureCloudInstance=yr,e.BrowserAuthError=Zi,e.BrowserAuthErrorCodes=Xi,e.BrowserCacheLocation=ws,e.BrowserConfigurationAuthError=wa,e.BrowserConfigurationAuthErrorCodes=ya,e.BrowserPerformanceClient=class extends Lo{constructor(e,r){super(e.auth.clientId,e.auth.authority||`${t}`,new pr(e.system?.loggerOptions||{},bc,Ac),bc,Ac,e.telemetry?.application||{appName:"",appVersion:""},r)}generateId(){return ia()}getPageVisibility(){return document.visibilityState?.toString()||null}getOnlineStatus(){return"undefined"!=typeof navigator?navigator.onLine:null}deleteIncompleteSubMeasurements(e){nl()?.then((t=>{const r=this.eventsByCorrelationId.get(e.event.correlationId),n=r&&r.eventId===e.event.eventId,o=[];n&&r?.incompleteSubMeasurements&&r.incompleteSubMeasurements.forEach((e=>{o.push({...e})})),t.BrowserPerformanceMeasurement.flushMeasurements(e.event.correlationId,o)}))}startMeasurement(e,t){const r=this.getPageVisibility(),n=this.getOnlineStatus(),o=super.startMeasurement(e,t),i=ol()?window.performance.now():void 0,s=nl()?.then((t=>new t.BrowserPerformanceMeasurement(e,o.event.correlationId)));return s?.then((e=>e.startMeasurement())),{...o,end:(e,t,a)=>{const c=function(){if("undefined"==typeof window||!window.navigator)return{};const e="connection"in window.navigator?window.navigator.connection:void 0;return{effectiveType:e?.effectiveType,rtt:e?.rtt}}(),h=o.end({...e,startPageVisibility:r,startOnlineStatus:n,endPageVisibility:this.getPageVisibility(),durationMs:il(i),networkEffectiveType:c.effectiveType,networkRtt:c.rtt},t,a);return s?.then((e=>e.endMeasurement())),this.deleteIncompleteSubMeasurements(o),h},discard:()=>{o.discard(),s?.then((e=>e.flushMeasurement())),this.deleteIncompleteSubMeasurements(o)}}}},e.BrowserPerformanceMeasurement=sl,e.BrowserRootPerformanceEvents=tc,e.BrowserUtils=Fa,e.CacheLookupPolicy=Hs,e.ClientAuthError=Te,e.ClientAuthErrorCodes=Et,e.ClientConfigurationError=Ce,e.ClientConfigurationErrorCodes=$e,e.DEFAULT_IFRAME_TIMEOUT_MS=1e4,e.EventHandler=qc,e.EventMessageUtils=class{static getInteractionStatusFromEvent(t,r){switch(t.eventType){case Tc.ACQUIRE_TOKEN_START:if(t.interactionType===e.InteractionType.Redirect||t.interactionType===e.InteractionType.Popup)return qs.AcquireToken;break;case Tc.HANDLE_REDIRECT_START:return qs.HandleRedirect;case Tc.LOGOUT_START:return qs.Logout;case Tc.LOGOUT_END:if(r&&r!==qs.Logout)break;return qs.None;case Tc.HANDLE_REDIRECT_END:if(r&&r!==qs.HandleRedirect)break;return qs.None;case Tc.ACQUIRE_TOKEN_SUCCESS:case Tc.ACQUIRE_TOKEN_FAILURE:case Tc.RESTORE_FROM_BFCACHE:if(t.interactionType===e.InteractionType.Redirect||t.interactionType===e.InteractionType.Popup){if(r&&r!==qs.AcquireToken)break;return qs.None}}return null}},e.EventType=Tc,e.InteractionRequiredAuthError=Qn,e.InteractionRequiredAuthErrorCodes=Jn,e.InteractionStatus=qs,e.JsonWebTokenTypes=dl,e.LocalStorage=vc,e.Logger=pr,e.MemoryStorage=Ba,e.NavigationClient=uh,e.OIDC_DEFAULT_SCOPES=ul,e.PromptValue=ll,e.ProtocolMode=Kr,e.PublicClientApplication=Vh,e.ResponseMode=hl,e.ServerError=Yn,e.SessionStorage=kc,e.SignedHttpRequest=class{constructor(e,t){const r=t&&t.loggerOptions||{};this.logger=new pr(r,bc,Ac),this.cryptoOps=new ja(this.logger),this.popTokenGenerator=new Un(this.cryptoOps,new Xr),this.shrParameters=e}async generatePublicKeyThumbprint(){const{kid:e}=await this.popTokenGenerator.generateKid(this.shrParameters);return e}async signRequest(e,t,r){return this.popTokenGenerator.signPayload(e,t,this.shrParameters,r)}async removeKeys(e,t){return this.cryptoOps.removeTokenBindingKey(e,t)}},e.StubPerformanceClient=Xr,e.WrapperSKU={React:"@azure/msal-react",Angular:"@azure/msal-angular"},e.createNestablePublicClientApplication=async function(e,t,r){const n=new Qh(e);if(await n.initialize(t),n.isAvailable()){const o=t||ia(),i=new Wh(n),s=r?r(e,i):new Vh(e,i);return await s.initialize({correlationId:o}),s}return Xh(e)},e.createStandardPublicClientApplication=Xh,e.enforceResourceParameter=So,e.isPlatformBrokerAvailable=async function(e,t,r){const n=new pr(e||{},bc,Ac),o=r||"";n.trace("07660b",o);const i=t||new Xr;return"undefined"==typeof window?(n.trace("082ed3",o),!1):!!await Ih(n,i,o)},e.loadExternalTokens=async function(e,t,r,n,o=new Xr){xa();const i=fh(e,!0),s=t.correlationId||ia(),a=o.startMeasurement(ec,s);try{const c=r.id_token?vr(r.id_token,js):void 0,h=kr(c||{}),l={protocolMode:i.system.protocolMode,knownAuthorities:i.auth.knownAuthorities,cloudDiscoveryMetadata:i.auth.cloudDiscoveryMetadata,authorityMetadata:i.auth.authorityMetadata},d=new pr(i.system.loggerOptions||{}),u=new ja(d,i.telemetry.client),g=new _c(i.auth.clientId,i.cache,u,d,i.telemetry.client,new qc(d),yo(i.auth)),p=t.authority||i.auth.authority,m=await wo(mo.generateAuthority(p,t.azureCloudOptions),i.system.networkClient,g,l,d,s,o),f=await qn(Zh,"loadAccount",d,o,s)(t,n.clientInfo||r.client_info||"",s,g,d,u,m,c,o),y=await qn(el,"loadIdToken",d,o,s)(r,f.homeAccountId,f.environment,f.realm,h,s,g,d,e.auth.clientId),w=await qn(tl,"loadAccessToken",d,o,s)(t,r,f.homeAccountId,f.environment,f.realm,h,n,s,g,d,e.auth.clientId),I=await qn(rl,"loadRefreshToken",d,o,s)(r,f.homeAccountId,f.environment,h,s,g,d,e.auth.clientId,o);return a.end({success:!0},void 0,Br(f)),function(e,t,r,n){let o,i="",s=[],a=null;t?.accessToken&&(i=t.accessToken.secret,s=Pt.fromString(t.accessToken.target).asArray(),a=dn(t.accessToken.expiresOn),o=dn(t.accessToken.extendedExpiresOn));const c=t.account;return{authority:r.canonicalAuthority,uniqueId:t.account.localAccountId,tenantId:t.account.realm,scopes:s,account:Br(c),idToken:t.idToken?.secret||"",idTokenClaims:n||{},accessToken:i,fromCache:!0,expiresOn:a,correlationId:e.correlationId||"",requestId:"",extExpiresOn:o,familyId:t.refreshToken?.familyId||"",tokenType:t?.accessToken?.tokenType||"",state:e.state||"",cloudGraphHostName:c.cloudGraphHostName||"",msGraphHost:c.msGraphHost||"",fromPlatformBroker:!1}}(t,{account:f,idToken:y,accessToken:w,refreshToken:I},m,c)}catch(e){throw a.end({success:!1},e),e}},e.stubbedPublicClientApplication=Yh,e.version=Ac})); diff --git a/v2/operator-app/src/App.tsx b/v2/operator-app/src/App.tsx new file mode 100644 index 0000000..8124259 --- /dev/null +++ b/v2/operator-app/src/App.tsx @@ -0,0 +1,38 @@ +import { Routes, Route } from 'react-router-dom'; +import ProtectedRoute from './auth/ProtectedRoute'; +import Shell from './components/Shell'; +import Login from './routes/login'; +import Home from './routes/home'; +import BriefsList from './routes/briefs/list'; +import BriefNew from './routes/briefs/new'; +import BriefDetail from './routes/briefs/detail'; +import ReportDetail from './routes/reports/detail'; +import TeamsList from './routes/teams/list'; +import TeamDetail from './routes/teams/detail'; +import AdminUsers from './routes/admin/users'; +import Help from './routes/help'; + +export default function App() { + return ( + + } /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/v2/operator-app/src/api/briefs.ts b/v2/operator-app/src/api/briefs.ts new file mode 100644 index 0000000..e74a8c0 --- /dev/null +++ b/v2/operator-app/src/api/briefs.ts @@ -0,0 +1,94 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { fetcher } from './client'; + +export type BriefSummary = { + id: string; + team_id: string; + owner_id: string; + slug: string; + client_name: string; + category: string; + business_question: string; + date_window_days: number; + budget_usd: number; + platforms: string[]; + positioning: { positioning?: string; brand?: BriefBrand } | null; + kpis: string[] | null; + context_vision: string | null; + min_likes: number; + min_plays: number; + min_stl_pct: number; + prior_report_id: string | null; + created_at: string; +}; + +export type BriefBrand = { name: string; handle: string; positioning?: string }; +export type BriefCompetitor = { name: string; handle: string }; +export type BriefAudience = { + primary: string; + secondary?: string; + age_range: string; + gender: string; + interests: string[]; +}; + +export type BriefCreateInput = { + client_name: string; + category: string; + brand: BriefBrand; + competitors: BriefCompetitor[]; + audience: BriefAudience; + geo: string; + language: string; + business_question: string; + kpis: string[]; + budget_usd: number; + date_window_days: number; + platforms: ('tiktok')[]; + context_vision?: string; + prior_report_id?: string | null; + min_likes: number; + min_plays: number; + min_stl_pct: number; +}; + +export type BriefIssue = { path: (string | number)[]; message: string; code?: string }; + +export function useBriefs() { + return useQuery<{ briefs: BriefSummary[] }>({ + queryKey: ['briefs'], + queryFn: () => fetcher('/briefs'), + }); +} + +export function useBrief(id: string | undefined) { + return useQuery<{ brief: BriefSummary }>({ + queryKey: ['brief', id], + queryFn: () => fetcher(`/briefs/${id}`), + enabled: Boolean(id), + }); +} + +export function useCreateBrief() { + const qc = useQueryClient(); + return useMutation<{ brief: BriefSummary }, Error, BriefCreateInput>({ + mutationFn: (input) => + fetcher('/briefs', { + method: 'POST', + body: JSON.stringify(input), + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['briefs'] }); + }, + }); +} + +export function useDeleteBrief() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id) => fetcher(`/briefs/${id}`, { method: 'DELETE' }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['briefs'] }); + }, + }); +} diff --git a/v2/operator-app/src/api/client.ts b/v2/operator-app/src/api/client.ts new file mode 100644 index 0000000..e85dd0e --- /dev/null +++ b/v2/operator-app/src/api/client.ts @@ -0,0 +1,46 @@ +import { QueryClient } from '@tanstack/react-query'; + +export type ApiIssue = { path: (string | number)[]; message: string; code?: string }; + +export class ApiError extends Error { + status: number; + issues?: ApiIssue[]; + constructor(status: number, message: string, issues?: ApiIssue[]) { + super(message); + this.status = status; + if (issues) this.issues = issues; + } +} + +export async function fetcher(path: string, init?: RequestInit): Promise { + const url = path.startsWith('/api') ? path : `/api${path.startsWith('/') ? path : `/${path}`}`; + const res = await fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...(init?.headers ?? {}), + }, + ...init, + }); + if (!res.ok) { + let msg = res.statusText; + let issues: ApiIssue[] | undefined; + try { + const body = await res.json(); + if (body?.error) msg = body.error; + if (Array.isArray(body?.issues)) issues = body.issues; + } catch {} + throw new ApiError(res.status, msg, issues); + } + if (res.status === 204) return undefined as T; + return (await res.json()) as T; +} + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, + retry: false, + }, + }, +}); diff --git a/v2/operator-app/src/auth/ProtectedRoute.tsx b/v2/operator-app/src/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..ce6634b --- /dev/null +++ b/v2/operator-app/src/auth/ProtectedRoute.tsx @@ -0,0 +1,37 @@ +import { useEffect, type ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; +import { useMe } from './useMe'; +import { ApiError } from '../api/client'; +import { useTeamStore } from '../store/team'; + +type Props = { children?: ReactNode }; + +export default function ProtectedRoute({ children }: Props) { + const { data, isLoading, error } = useMe(); + const setUser = useTeamStore((s) => s.setUser); + const setActiveTeam = useTeamStore((s) => s.setActiveTeam); + + useEffect(() => { + if (data) { + setUser(data.user); + setActiveTeam(data.active_team); + } + }, [data, setUser, setActiveTeam]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error instanceof ApiError && error.status === 401) { + return ; + } + if (error) { + return ; + } + + return <>{children}; +} diff --git a/v2/operator-app/src/auth/msal.ts b/v2/operator-app/src/auth/msal.ts new file mode 100644 index 0000000..fd00de6 --- /dev/null +++ b/v2/operator-app/src/auth/msal.ts @@ -0,0 +1,59 @@ +declare global { + interface Window { + msal?: any; + } +} + +const tenantId = import.meta.env.VITE_AZURE_TENANT_ID as string | undefined; +const clientId = import.meta.env.VITE_AZURE_CLIENT_ID as string | undefined; + +let pca: any = null; + +async function ensureMsalLoaded(): Promise { + if (window.msal) return; + await new Promise((resolve, reject) => { + const s = document.createElement('script'); + s.src = '/msal-browser.min.js'; + s.async = true; + s.onload = () => resolve(); + s.onerror = () => reject(new Error('Failed to load msal-browser.min.js')); + document.head.appendChild(s); + }); +} + +export async function getMsal() { + if (pca) return pca; + await ensureMsalLoaded(); + if (!tenantId || !clientId) { + throw new Error('Missing VITE_AZURE_TENANT_ID or VITE_AZURE_CLIENT_ID'); + } + pca = new window.msal.PublicClientApplication({ + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + redirectUri: window.location.origin + '/login', + }, + cache: { cacheLocation: 'sessionStorage' }, + }); + await pca.initialize(); + return pca; +} + +export async function loginWithMicrosoft(): Promise { + const app = await getMsal(); + await app.loginRedirect({ scopes: ['openid', 'profile', 'email'] }); +} + +export async function handleRedirectAndExchange(): Promise<{ ok: boolean } | null> { + const app = await getMsal(); + const result = await app.handleRedirectPromise(); + if (!result?.idToken) return null; + const res = await fetch('/api/sso/token-exchange', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idToken: result.idToken }), + }); + if (!res.ok) throw new Error('Token exchange failed'); + return res.json(); +} diff --git a/v2/operator-app/src/auth/useMe.ts b/v2/operator-app/src/auth/useMe.ts new file mode 100644 index 0000000..40e51a0 --- /dev/null +++ b/v2/operator-app/src/auth/useMe.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetcher } from '../api/client'; +import type { User, Team } from '../store/team'; + +export type MeResponse = { + user: User; + memberships: Team[]; + active_team: Team | null; +}; + +export function useMe() { + return useQuery({ + queryKey: ['me'], + queryFn: () => fetcher('/me'), + }); +} diff --git a/v2/operator-app/src/components/Header.tsx b/v2/operator-app/src/components/Header.tsx new file mode 100644 index 0000000..26e7a4f --- /dev/null +++ b/v2/operator-app/src/components/Header.tsx @@ -0,0 +1,26 @@ +import { useMe } from '../auth/useMe'; +import TeamSwitcher from './TeamSwitcher'; + +export default function Header() { + const { data } = useMe(); + const user = data?.user; + + return ( +
+
+ Social Listening + V2 +
+
+ + {user?.email ?? ''} + + Sign out + +
+
+ ); +} diff --git a/v2/operator-app/src/components/Shell.tsx b/v2/operator-app/src/components/Shell.tsx new file mode 100644 index 0000000..56b1f61 --- /dev/null +++ b/v2/operator-app/src/components/Shell.tsx @@ -0,0 +1,17 @@ +import { Outlet } from 'react-router-dom'; +import Header from './Header'; +import Sidebar from './Sidebar'; + +export default function Shell() { + return ( +
+
+
+ +
+ +
+
+
+ ); +} diff --git a/v2/operator-app/src/components/Sidebar.tsx b/v2/operator-app/src/components/Sidebar.tsx new file mode 100644 index 0000000..d7b03c1 --- /dev/null +++ b/v2/operator-app/src/components/Sidebar.tsx @@ -0,0 +1,28 @@ +import { NavLink } from 'react-router-dom'; +import { useMe } from '../auth/useMe'; + +const linkBase = + 'block px-4 py-2 rounded text-sm transition-colors'; +const linkInactive = 'text-text-muted hover:bg-bg-field hover:text-text-body'; +const linkActive = 'bg-bg-field text-accent'; + +function cls({ isActive }: { isActive: boolean }) { + return `${linkBase} ${isActive ? linkActive : linkInactive}`; +} + +export default function Sidebar() { + const { data } = useMe(); + const isSuper = data?.user?.is_super_admin === true; + + return ( + + ); +} diff --git a/v2/operator-app/src/components/TeamSwitcher.tsx b/v2/operator-app/src/components/TeamSwitcher.tsx new file mode 100644 index 0000000..2b860b7 --- /dev/null +++ b/v2/operator-app/src/components/TeamSwitcher.tsx @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMe } from '../auth/useMe'; +import { fetcher } from '../api/client'; + +export default function TeamSwitcher() { + const { data } = useMe(); + const qc = useQueryClient(); + const mut = useMutation({ + mutationFn: (team_id: string) => + fetcher('/me/active-team', { + method: 'PATCH', + body: JSON.stringify({ team_id }), + }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['me'] }), + }); + + if (!data || data.memberships.length === 0) return null; + + return ( + + ); +} diff --git a/v2/operator-app/src/main.tsx b/v2/operator-app/src/main.tsx new file mode 100644 index 0000000..79dec8e --- /dev/null +++ b/v2/operator-app/src/main.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import { queryClient } from './api/client'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + , +); diff --git a/v2/operator-app/src/routes/admin/users.tsx b/v2/operator-app/src/routes/admin/users.tsx new file mode 100644 index 0000000..2927207 --- /dev/null +++ b/v2/operator-app/src/routes/admin/users.tsx @@ -0,0 +1,40 @@ +export default function AdminUsers() { + const users: Array<{ id: string; email: string; is_super_admin: boolean }> = []; + + return ( +
+

Admin: Users

+
+ + + + + + + + + + {users.length === 0 && ( + + + + )} + {users.map((u) => ( + + + + + + ))} + +
EmailSuper-adminActions
+ Phase A scaffold. Will read /api/admin/users. +
{u.email}{u.is_super_admin ? 'Yes' : 'No'} + +
+
+
+ ); +} diff --git a/v2/operator-app/src/routes/help.tsx b/v2/operator-app/src/routes/help.tsx new file mode 100644 index 0000000..aa9a9fc --- /dev/null +++ b/v2/operator-app/src/routes/help.tsx @@ -0,0 +1,191 @@ +export default function Help() { + return ( +
+

Help & FAQ

+ +
+

V2 — what's new

+

+ Multi-team workspaces, Microsoft SSO, configurable engagement-quality floor, + manifest-gated linking, React dashboard. +

+
+ +
+

How It Works

+

+ The pipeline runs 8 stages automatically. You fill in a brief, hit Run, + and get a client-ready report with trends, audience insights, content + opportunities, and creator spotlights. +

+
+ {[ + { range: '1-2', label: 'Brief & Strategy' }, + { range: '3-5', label: 'Scrape & Enrich' }, + { range: '6-7', label: 'Review & Research' }, + { range: '8', label: 'Final Report' }, + ].map((s) => ( +
+
{s.range}
+
{s.label}
+
+ ))} +
+
+ +
+

Brief Fields Guide

+ + + + + + + + + + +
+ +
+

Tips for Better Reports

+ + + + + + +
+ +
+

What Each Stage Does

+ + + + + + + + +
+ +
+

FAQ

+ + + + + +
+
+ ); +} + +function FieldGuide({ name, body, example, tip }: { + name: string; body: string; example?: string; tip?: string; +}) { + return ( +
+
{name}
+

{body}

+ {example &&

Example: {example}

} + {tip &&

Tip: {tip}

} +
+ ); +} + +function Tip({ title, body }: { title: string; body: string }) { + return ( +
+
{title}
+

{body}

+
+ ); +} + +function Stage({ n, title, body }: { n: string; title: string; body: string }) { + return ( +
+
+ Stage {n} — {title} +
+

{body}

+
+ ); +} + +function Faq({ q, a }: { q: string; a: string }) { + return ( +
+
{q}
+

{a}

+
+ ); +} diff --git a/v2/operator-app/src/routes/home.tsx b/v2/operator-app/src/routes/home.tsx new file mode 100644 index 0000000..d6289ea --- /dev/null +++ b/v2/operator-app/src/routes/home.tsx @@ -0,0 +1,22 @@ +import { useTeamStore } from '../store/team'; + +export default function Home() { + const activeTeam = useTeamStore((s) => s.activeTeam); + + return ( +
+
+

Home

+ {activeTeam && ( + + {activeTeam.name} + + )} +
+
+

Recent reports

+

Phase A scaffold. Recent reports list will live here.

+
+
+ ); +} diff --git a/v2/operator-app/src/routes/login.tsx b/v2/operator-app/src/routes/login.tsx new file mode 100644 index 0000000..439f93c --- /dev/null +++ b/v2/operator-app/src/routes/login.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { handleRedirectAndExchange, loginWithMicrosoft } from '../auth/msal'; +import { fetcher } from '../api/client'; + +export default function Login() { + const [params] = useSearchParams(); + const showPassword = params.get('password') === '1'; + const navigate = useNavigate(); + const [err, setErr] = useState(null); + const [busy, setBusy] = useState(false); + + useEffect(() => { + handleRedirectAndExchange() + .then((r) => { if (r?.ok) navigate('/', { replace: true }); }) + .catch((e) => setErr(e.message ?? 'Sign-in failed')); + }, [navigate]); + + async function onSso() { + setErr(null); setBusy(true); + try { await loginWithMicrosoft(); } + catch (e: any) { setErr(e?.message ?? 'SSO failed'); setBusy(false); } + } + + return ( +
+
+
+
Social Listening
+
V2 Operator
+
+ + {err &&
{err}
} + {showPassword && } +
+
+ ); +} + +function PasswordFallback({ onError }: { onError: (s: string | null) => void }) { + const navigate = useNavigate(); + const [pw, setPw] = useState(''); + const [busy, setBusy] = useState(false); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + onError(null); setBusy(true); + try { + await fetcher('/login', { method: 'POST', body: JSON.stringify({ password: pw }) }); + navigate('/', { replace: true }); + } catch (err: any) { + onError(err?.message ?? 'Login failed'); + } finally { setBusy(false); } + } + + return ( +
+
Emergency password
+ setPw(e.target.value)} + className="w-full bg-bg-field border border-border-input rounded px-3 py-2 text-sm" + placeholder="Password" + /> + +
+ ); +} diff --git a/v2/operator-app/src/routes/reports/detail.tsx b/v2/operator-app/src/routes/reports/detail.tsx new file mode 100644 index 0000000..eab81ca --- /dev/null +++ b/v2/operator-app/src/routes/reports/detail.tsx @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +export default function ReportDetail() { + const { id } = useParams(); + return ( +
+

Report {id}

+
+

+ Phase A scaffold. Live SSE feed of pipeline events + final report viewer link + will live here. +

+
+
+ ); +} diff --git a/v2/operator-app/src/routes/teams/detail.tsx b/v2/operator-app/src/routes/teams/detail.tsx new file mode 100644 index 0000000..832e1b7 --- /dev/null +++ b/v2/operator-app/src/routes/teams/detail.tsx @@ -0,0 +1,16 @@ +import { useParams } from 'react-router-dom'; + +export default function TeamDetail() { + const { id } = useParams(); + return ( +
+

Team {id}

+
+

+ Phase A scaffold. Team detail (members, invites, role management + owner/admin/member) will live here. +

+
+
+ ); +} diff --git a/v2/operator-app/src/routes/teams/list.tsx b/v2/operator-app/src/routes/teams/list.tsx new file mode 100644 index 0000000..9c0def2 --- /dev/null +++ b/v2/operator-app/src/routes/teams/list.tsx @@ -0,0 +1,12 @@ +export default function TeamsList() { + return ( +
+

Teams

+
+

+ Phase A scaffold. Teams list with create-team button will live here. +

+
+
+ ); +} diff --git a/v2/operator-app/src/store/team.ts b/v2/operator-app/src/store/team.ts new file mode 100644 index 0000000..711facc --- /dev/null +++ b/v2/operator-app/src/store/team.ts @@ -0,0 +1,30 @@ +import { create } from 'zustand'; + +export type User = { + id: string; + email: string; + display_name?: string; + is_super_admin?: boolean; +}; + +export type Team = { + id: string; + name: string; + role?: string; +}; + +type TeamState = { + user: User | null; + activeTeam: Team | null; + setUser: (u: User | null) => void; + setActiveTeam: (t: Team | null) => void; + clear: () => void; +}; + +export const useTeamStore = create((set) => ({ + user: null, + activeTeam: null, + setUser: (user) => set({ user }), + setActiveTeam: (activeTeam) => set({ activeTeam }), + clear: () => set({ user: null, activeTeam: null }), +})); diff --git a/v2/operator-app/src/styles.css b/v2/operator-app/src/styles.css new file mode 100644 index 0000000..fac7115 --- /dev/null +++ b/v2/operator-app/src/styles.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#root { + height: 100%; +} + +body { + background-color: #0a0a0a; + color: #e0e0e0; + font-family: 'Montserrat', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/v2/operator-app/tailwind.config.ts b/v2/operator-app/tailwind.config.ts new file mode 100644 index 0000000..7658a18 --- /dev/null +++ b/v2/operator-app/tailwind.config.ts @@ -0,0 +1,35 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: ['./index.html', './src/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + bg: { + base: '#0a0a0a', + panel: '#141414', + field: '#1a1a1a', + }, + border: { + subtle: '#2a2a2a', + input: '#333', + }, + accent: { + DEFAULT: '#f5a623', + hover: '#e69920', + }, + text: { + body: '#e0e0e0', + muted: '#888', + dim: '#666', + }, + }, + fontFamily: { + sans: ['Montserrat', 'system-ui', 'sans-serif'], + }, + }, + }, + plugins: [], +}; + +export default config; diff --git a/v2/operator-app/tsconfig.json b/v2/operator-app/tsconfig.json new file mode 100644 index 0000000..0e42bc0 --- /dev/null +++ b/v2/operator-app/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "useDefineForClassFields": true, + "allowImportingTsExtensions": false, + "isolatedModules": true, + "noEmit": true, + "types": ["vite/client"] + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/v2/operator-app/tsconfig.node.json b/v2/operator-app/tsconfig.node.json new file mode 100644 index 0000000..c1e22c7 --- /dev/null +++ b/v2/operator-app/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["vite.config.ts", "tailwind.config.ts", "postcss.config.js"] +} diff --git a/v2/operator-app/vite.config.ts b/v2/operator-app/vite.config.ts new file mode 100644 index 0000000..9d9460a --- /dev/null +++ b/v2/operator-app/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3457', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/v2/package-lock.json b/v2/package-lock.json new file mode 100644 index 0000000..7573cb0 --- /dev/null +++ b/v2/package-lock.json @@ -0,0 +1,4137 @@ +{ + "name": "social-reporting-v2", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "social-reporting-v2", + "version": "0.1.0", + "workspaces": [ + "operator-app" + ], + "dependencies": { + "postgres": "^3.4.8", + "tsx": "^4.7.0", + "typescript": "^5.4.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "vitest": "^1.6.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.6.tgz", + "integrity": "sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.6.tgz", + "integrity": "sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", + "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/operator-app": { + "resolved": "operator-app", + "link": true + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "operator-app": { + "version": "0.1.0", + "dependencies": { + "@tanstack/react-query": "^5.51.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.4.0", + "vite": "^5.4.0" + } + } + } +} diff --git a/v2/package.json b/v2/package.json new file mode 100644 index 0000000..09f58f8 --- /dev/null +++ b/v2/package.json @@ -0,0 +1,28 @@ +{ + "name": "social-reporting-v2", + "version": "0.1.0", + "private": true, + "type": "module", + "workspaces": [ + "operator-app" + ], + "scripts": { + "server": "tsx watch server/index.ts", + "server:prod": "tsx server/index.ts", + "pipe": "tsx pipeline/cli.ts", + "ui:dev": "npm run dev --workspace operator-app", + "ui:build": "npm run build --workspace operator-app", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "postgres": "^3.4.8", + "tsx": "^4.7.0", + "typescript": "^5.4.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "vitest": "^1.6.0" + } +} diff --git a/v2/pipeline/.gitkeep b/v2/pipeline/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/v2/pipeline/__tests__/engagement_floor.test.ts b/v2/pipeline/__tests__/engagement_floor.test.ts new file mode 100644 index 0000000..d5368b2 --- /dev/null +++ b/v2/pipeline/__tests__/engagement_floor.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { applyEngagementFloor, computeStlPct, type EngagementFloor } from '../lib/engagement_floor.js'; + +const BASE: EngagementFloor = { min_likes: 1000, min_plays: 10000, min_stl_pct: 0 }; + +describe('applyEngagementFloor', () => { + it('keeps items above the floor', () => { + const items = [ + { plays: 50000, likes: 5000 }, + { plays: 100000, likes: 8000 }, + ]; + const { kept, counters } = applyEngagementFloor(items, BASE); + expect(kept).toHaveLength(2); + expect(counters.kept_after_floor).toBe(2); + expect(counters.dropped_min_likes + counters.dropped_min_plays).toBe(0); + }); + + it('drops by min_plays', () => { + const items = [{ plays: 5000, likes: 5000 }]; + const { kept, counters } = applyEngagementFloor(items, BASE); + expect(kept).toHaveLength(0); + expect(counters.dropped_min_plays).toBe(1); + }); + + it('drops by min_likes', () => { + const items = [{ plays: 50000, likes: 50 }]; + const { kept, counters } = applyEngagementFloor(items, BASE); + expect(kept).toHaveLength(0); + expect(counters.dropped_min_likes).toBe(1); + }); + + it('drops zero-plays items as a special case', () => { + const items = [{ plays: 0, likes: 99999 }, { plays: -1, likes: 99999 }]; + const { counters } = applyEngagementFloor(items, BASE); + expect(counters.dropped_zero_plays).toBe(2); + }); + + it('applies stl% floor when configured', () => { + const items = [ + // STL = (1000+0+0+0)/10000 * 100 = 10% + { plays: 10000, likes: 1000, saves: 0, comments_count: 0, shares: 0 }, + // STL = (1000+0+0+0)/100000 * 100 = 1% + { plays: 100000, likes: 1000, saves: 0, comments_count: 0, shares: 0 }, + ]; + const { kept, counters } = applyEngagementFloor(items, { ...BASE, min_stl_pct: 5 }); + expect(kept).toHaveLength(1); + expect(kept[0]?.plays).toBe(10000); + expect(counters.dropped_min_stl).toBe(1); + }); + + it('counters sum to raw_returned', () => { + const items = [ + { plays: 0, likes: 0 }, + { plays: 5000, likes: 5000 }, + { plays: 50000, likes: 50 }, + { plays: 100000, likes: 5000 }, + ]; + const { counters } = applyEngagementFloor(items, BASE); + const total = counters.dropped_zero_plays + counters.dropped_min_plays + + counters.dropped_min_likes + counters.dropped_min_stl + counters.kept_after_floor; + expect(total).toBe(counters.raw_returned); + }); +}); + +describe('computeStlPct', () => { + it('returns 0 for zero plays', () => { + expect(computeStlPct({ plays: 0, likes: 100 })).toBe(0); + }); + + it('sums likes+saves+comments+shares', () => { + const stl = computeStlPct({ plays: 1000, likes: 50, saves: 30, comments_count: 10, shares: 10 }); + expect(stl).toBe(10); // 100/1000 * 100 + }); +}); diff --git a/v2/pipeline/__tests__/ids.test.ts b/v2/pipeline/__tests__/ids.test.ts new file mode 100644 index 0000000..4c3e28e --- /dev/null +++ b/v2/pipeline/__tests__/ids.test.ts @@ -0,0 +1,70 @@ +// Comprehensive URL-form fixture for extractTikTokId. +// Every form V1 has seen drift in goes here. Add new mutation forms here, not in code. +import { describe, it, expect } from 'vitest'; +import { extractTikTokId, canonicalTikTokUrl } from '../lib/ids.js'; + +const HANDLED = [ + // Standard www form + ['https://www.tiktok.com/@dove/video/7280000000000000000', '7280000000000000000'], + // Without www + ['https://tiktok.com/@dove/video/7280000000000000000', '7280000000000000000'], + // With trailing query params (most common drift cause) + ['https://www.tiktok.com/@dove/video/7280000000000000000?is_from_webapp=1&sender_device=pc', '7280000000000000000'], + // Trailing slash + ['https://www.tiktok.com/@dove/video/7280000000000000000/', '7280000000000000000'], + // Mobile m.tiktok.com /v/.html form + ['https://m.tiktok.com/v/7280000000000000000.html', '7280000000000000000'], + // Older share /t/ form + ['https://www.tiktok.com/t/7280000000000000000', '7280000000000000000'], + // Bare numeric id + ['7280000000000000000', '7280000000000000000'], + // 18-digit ids (older content) + ['https://www.tiktok.com/@dove/video/728000000000000000', '728000000000000000'], + // Capital case in handle (drift case from V1) + ['https://www.tiktok.com/@DoveBeauty/video/7280000000000000000', '7280000000000000000'], + // Embedded in JSON-ish text (Apify response field bleed) + ['"webVideoUrl":"https://www.tiktok.com/@dove/video/7280000000000000000"', '7280000000000000000'], +]; + +const REJECTED: Array<[string | null | undefined | number, string]> = [ + [null, 'null input'], + [undefined, 'undefined input'], + ['', 'empty string'], + [' ', 'whitespace only'], + ['https://www.tiktok.com/@dove', 'profile URL, no video id'], + ['https://vm.tiktok.com/ZMabc123/', 'short link — needs resolveShortLink first'], + ['https://example.com/video/12345', 'wrong domain pattern OK in URL but id length must be 15-21'], + [123, 'numeric input but too short'], +]; + +describe('extractTikTokId', () => { + it.each(HANDLED)('extracts %s → %s', (input, expected) => { + expect(extractTikTokId(input)).toBe(expected); + }); + + it.each(REJECTED)('rejects %p (%s)', (input, _reason) => { + expect(extractTikTokId(input)).toBeNull(); + }); + + it('extracts the SAME id from every shape — the linking-fix invariant', () => { + const ids = HANDLED.map(([url]) => extractTikTokId(url)); + const targetId = HANDLED[0]![1]; + // The first 7 fixture rows all share the canonical 7280... id. + const sameAsTarget = HANDLED.slice(0, 7).every(([_url], i) => ids[i] === targetId); + expect(sameAsTarget).toBe(true); + }); +}); + +describe('canonicalTikTokUrl', () => { + it('round-trips id through canonical URL', () => { + const url = canonicalTikTokUrl('7280000000000000000', 'dove'); + expect(url).toBe('https://www.tiktok.com/@dove/video/7280000000000000000'); + expect(extractTikTokId(url)).toBe('7280000000000000000'); + }); + + it('strips a leading @ from handle', () => { + expect(canonicalTikTokUrl('7280000000000000000', '@dove')).toBe( + 'https://www.tiktok.com/@dove/video/7280000000000000000', + ); + }); +}); diff --git a/v2/pipeline/__tests__/linking_fix.test.ts b/v2/pipeline/__tests__/linking_fix.test.ts new file mode 100644 index 0000000..979bc7e --- /dev/null +++ b/v2/pipeline/__tests__/linking_fix.test.ts @@ -0,0 +1,96 @@ +// THE LINKING FIX TEST. +// V1 bug: assets joined to videos via Map.get(url) silently dropped on URL-form drift. +// V2 fix: every Apify response is matched back to the canonical TikTok id via +// extractTikTokId, mismatches go to drift_log.jsonl, never silently null. +// +// This test simulates exactly the kind of drift V1 saw — same logical video, different +// URL shapes returned by different Apify actors — and proves V2 collapses them to one id. +import { describe, it, expect, beforeEach } from 'vitest'; +import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { groupByCanonicalId } from '../stages/stage_4_pass2_enrich.js'; +import { resetDriftCounter, getDriftCount } from '../lib/drift_log.js'; +import { PATHS } from '../lib/paths.js'; + +const FIXTURE_ROOT = resolve('briefs/__linking_test_root__'); +const REPORT_ID = 'r1'; + +beforeEach(() => { + process.env.BRIEFS_ROOT = FIXTURE_ROOT; + if (existsSync(FIXTURE_ROOT)) rmSync(FIXTURE_ROOT, { recursive: true }); + mkdirSync(FIXTURE_ROOT, { recursive: true }); + resetDriftCounter(); +}); + +describe('groupByCanonicalId — the V1-bug fix', () => { + const VIDEO_ID = '7280000000000000000'; + const SELECTION = new Set([VIDEO_ID, '7281111111111111111']); + + it('matches the SAME id from every URL form V1 has seen drift in', () => { + const items = [ + { webVideoUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000' }, + { videoUrl: 'https://tiktok.com/@dove/video/7280000000000000000' }, + { url: 'https://www.tiktok.com/@dove/video/7280000000000000000?is_from_webapp=1' }, + { postUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000/' }, + { webVideoUrl: 'https://m.tiktok.com/v/7280000000000000000.html' }, + { url: 'https://www.tiktok.com/@DoveBeauty/video/7280000000000000000' }, + ]; + const grouped = groupByCanonicalId(REPORT_ID, 'TIKTOK_TRANSCRIPTS', items, SELECTION); + + // Every input collapses to the SAME bucket. V1's bug = these would have ended up + // in 6 different (or no!) buckets and most assets would silently null. + expect(grouped.size).toBe(1); + expect(grouped.get(VIDEO_ID)).toHaveLength(items.length); + expect(getDriftCount()).toBe(0); + }); + + it('logs drift loudly (not silently) when an item has no extractable id', () => { + const items = [ + { videoUrl: 'https://www.tiktok.com/@dove' }, // profile URL, no id + { videoUrl: 'https://vm.tiktok.com/ZMabc123/' }, // unresolved short link + { videoUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000' }, // valid one + ]; + const grouped = groupByCanonicalId(REPORT_ID, 'TIKTOK_COMMENTS', items, SELECTION); + + expect(grouped.size).toBe(1); + expect(grouped.get(VIDEO_ID)).toHaveLength(1); + expect(getDriftCount()).toBe(2); + + const log = readFileSync(PATHS.driftLog(REPORT_ID), 'utf-8'); + const lines = log.trim().split('\n'); + expect(lines).toHaveLength(2); + const events = lines.map((l) => JSON.parse(l)); + expect(events.every((e) => e.actor === 'TIKTOK_COMMENTS')).toBe(true); + expect(events.every((e) => e.reason === 'no-id-extracted')).toBe(true); + }); + + it('logs drift when actor returns a video that was not selected (out-of-set)', () => { + const items = [ + // valid + in selection + { webVideoUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000' }, + // valid id but NOT in our selection — must not silently land in any bucket + { webVideoUrl: 'https://www.tiktok.com/@other/video/7299999999999999999' }, + ]; + const grouped = groupByCanonicalId(REPORT_ID, 'TIKTOK_TRANSCRIPTS', items, SELECTION); + + expect(grouped.size).toBe(1); + expect(grouped.get(VIDEO_ID)).toHaveLength(1); + expect(grouped.has('7299999999999999999')).toBe(false); + expect(getDriftCount()).toBe(1); + + const log = readFileSync(PATHS.driftLog(REPORT_ID), 'utf-8').trim(); + const event = JSON.parse(log); + expect(event.reason).toBe('id-not-in-selection'); + expect(event.extracted_id).toBe('7299999999999999999'); + }); + + it('groups multiple items that legitimately point at the same video (e.g. duplicate transcripts)', () => { + const items = [ + { videoUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000', text: 'first' }, + { videoUrl: 'https://www.tiktok.com/@dove/video/7280000000000000000?_t=abc', text: 'second' }, + ]; + const grouped = groupByCanonicalId(REPORT_ID, 'TIKTOK_TRANSCRIPTS', items, SELECTION); + expect(grouped.get(VIDEO_ID)).toHaveLength(2); + expect(getDriftCount()).toBe(0); + }); +}); diff --git a/v2/pipeline/__tests__/manifest.test.ts b/v2/pipeline/__tests__/manifest.test.ts new file mode 100644 index 0000000..bec893c --- /dev/null +++ b/v2/pipeline/__tests__/manifest.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { buildManifest } from '../lib/manifest.js'; + +const FIXTURE_ROOT = resolve('briefs/__manifest_test_root__'); +const REPORT_ID = 'r1'; + +function fakeBundle(id: string, opts: { transcript?: boolean; commentsCount?: number; framesCount?: number; coverBytes?: number } = {}) { + const dir = join(FIXTURE_ROOT, REPORT_ID, 'enriched', id); + mkdirSync(join(dir, 'frames'), { recursive: true }); + writeFileSync(join(dir, 'metadata.json'), JSON.stringify({ id })); + + if ((opts.coverBytes ?? 10_000) > 0) { + writeFileSync(join(dir, 'cover.jpg'), Buffer.alloc(opts.coverBytes ?? 10_000, 0xff)); + } + + if (opts.transcript !== false) { + writeFileSync(join(dir, 'transcript.json'), JSON.stringify({ + language_detected: 'en', + text_original: 'hello world', + text_en: 'hello world', + source: 'apify-tiktok-subtitles', + })); + } + + const cn = opts.commentsCount ?? 7; + if (cn > 0) { + const comments = Array.from({ length: cn }, (_, i) => ({ + rank: i + 1, author_handle: `u${i}`, text_original: `c${i}`, text_en: `c${i}`, + likes: 100 - i, replies_count: 0, posted_at: '', + })); + writeFileSync(join(dir, 'comments.json'), JSON.stringify(comments)); + } + + const fc = opts.framesCount ?? 5; + for (let i = 1; i <= fc; i++) { + writeFileSync(join(dir, 'frames', `${String(i).padStart(4, '0')}.jpg`), Buffer.alloc(1024)); + } + + writeFileSync(join(dir, 'bundle.json'), JSON.stringify({ + id, + metadata: { id }, + transcript: opts.transcript === false ? null : { text_en: 'hello world' }, + comments: Array.from({ length: cn }, () => ({ text_en: 'x' })), + frames: Array.from({ length: fc }, (_, i) => ({ index: i + 1, path: `frames/${String(i + 1).padStart(4, '0')}.jpg` })), + cover_local: 'cover.jpg', + _validation: { all_ok: true, missing: [] }, + })); +} + +beforeEach(() => { + process.env.BRIEFS_ROOT = FIXTURE_ROOT; + if (existsSync(FIXTURE_ROOT)) rmSync(FIXTURE_ROOT, { recursive: true }); + mkdirSync(FIXTURE_ROOT, { recursive: true }); +}); + +describe('buildManifest', () => { + it('passes with three fully-bundled videos', () => { + fakeBundle('111111111111111111'); + fakeBundle('222222222222222222'); + fakeBundle('333333333333333333'); + const m = buildManifest(REPORT_ID, ['111111111111111111', '222222222222222222', '333333333333333333']); + expect(m.summary.all_ok).toBe(3); + expect(m.summary.coverage_pct).toBe(100); + for (const v of m.videos) { + expect(v.all_ok).toBe(true); + expect(v.missing).toHaveLength(0); + } + }); + + it('flags missing transcript', () => { + fakeBundle('111111111111111111'); + fakeBundle('222222222222222222', { transcript: false }); + const m = buildManifest(REPORT_ID, ['111111111111111111', '222222222222222222']); + expect(m.summary.all_ok).toBe(1); + expect(m.summary.coverage_pct).toBe(50); + const v = m.videos.find((x) => x.id === '222222222222222222'); + expect(v?.missing).toContain('transcript'); + }); + + it('flags too few comments (<5)', () => { + fakeBundle('111111111111111111', { commentsCount: 3 }); + const m = buildManifest(REPORT_ID, ['111111111111111111']); + const v = m.videos[0]!; + expect(v.comments.ok).toBe(false); + expect(v.missing).toContain('comments'); + }); + + it('flags too few frames (<3)', () => { + fakeBundle('111111111111111111', { framesCount: 2 }); + const m = buildManifest(REPORT_ID, ['111111111111111111']); + const v = m.videos[0]!; + expect(v.frames.ok).toBe(false); + expect(v.missing).toContain('frames'); + }); + + it('flags too-small cover (<5KB)', () => { + fakeBundle('111111111111111111', { coverBytes: 1000 }); + const m = buildManifest(REPORT_ID, ['111111111111111111']); + const v = m.videos[0]!; + expect(v.cover.ok).toBe(false); + expect(v.missing).toContain('cover'); + }); + + it('coverage_pct is a clean percentage', () => { + fakeBundle('111111111111111111'); + fakeBundle('222222222222222222'); + fakeBundle('333333333333333333', { transcript: false }); + fakeBundle('444444444444444444'); + const m = buildManifest(REPORT_ID, ['111111111111111111', '222222222222222222', '333333333333333333', '444444444444444444']); + expect(m.summary.coverage_pct).toBe(75); + }); +}); diff --git a/v2/pipeline/__tests__/mom_compare.test.ts b/v2/pipeline/__tests__/mom_compare.test.ts new file mode 100644 index 0000000..5953678 --- /dev/null +++ b/v2/pipeline/__tests__/mom_compare.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { runMomCompare } from '../lib/mom_compare.js'; +import type { Trend } from '../stages/stage_8_trends.js'; + +const FIXTURE_ROOT = resolve('briefs/__mom_test_root__'); + +function trend(id: string, name: string, category: string, videos: string[], plays: number): Trend { + return { + trend_id: id, + slug: name.toLowerCase().replace(/\s+/g, '-'), + name, + category, + narrative: 'placeholder narrative for testing.', + lens_tags: ['narrative'], + top_atomic_ids: [], + supporting_video_ids: videos, + business_question_relevance: { score: 0.7, tier: 'core', justification: 'test' }, + kpis: { + plays_total: plays, videos: videos.length, unique_creators: videos.length, + avg_stl_pct: 5, + paid_organic_split: { paid: 0, organic: videos.length, unclear: 0 }, + }, + }; +} + +function setupReport(reportId: string, trends: Trend[]) { + const dir = join(FIXTURE_ROOT, reportId); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'trends.json'), JSON.stringify(trends, null, 2)); +} + +beforeEach(() => { + process.env.BRIEFS_ROOT = FIXTURE_ROOT; + if (existsSync(FIXTURE_ROOT)) rmSync(FIXTURE_ROOT, { recursive: true }); + mkdirSync(FIXTURE_ROOT, { recursive: true }); +}); + +describe('runMomCompare', () => { + it('classifies new / returning / faded correctly', async () => { + const prior = [ + trend('TR-001', 'The Ceremonial Hair Wash', 'Hair Rituals', ['v1', 'v2', 'v3'], 1_000_000), + trend('TR-002', 'Anti-Influencer Beauty', 'Anti-Beauty Backlash', ['v4', 'v5'], 500_000), + trend('TR-003', 'Dropped Trend', 'Old Category', ['v9'], 100_000), + ]; + const current = [ + // returning: same name + shared videos + trend('TR-100', 'The Ceremonial Hair Wash', 'Hair Rituals', ['v1', 'v2', 'v6'], 1_500_000), + // new: completely different + trend('TR-101', 'Scalp as Self', 'Hair Rituals', ['v7', 'v8'], 700_000), + ]; + setupReport('current', current); + setupReport('prior', prior); + + const { result } = await runMomCompare('current', 'prior'); + + expect(result.returning_trends).toHaveLength(1); + const ret = result.returning_trends[0]!; + expect(ret.trend_id).toBe('TR-100'); + expect(ret.prior_trend_id).toBe('TR-001'); + expect(ret.velocity_delta.plays_total_pct).toBe(50); // (1.5M - 1M) / 1M = 50% + + expect(result.new_trends).toHaveLength(1); + expect(result.new_trends[0]?.trend_id).toBe('TR-101'); + + expect(result.faded_trends.map((f) => f.prior_trend_id).sort()).toEqual(['TR-002', 'TR-003']); + }); + + it('fails loudly when prior report does not exist', async () => { + setupReport('current', [trend('TR-1', 'X', 'A', ['v1'], 1)]); + await expect(runMomCompare('current', 'missing')).rejects.toThrow(/Prior report 'missing' not found/); + }); + + it('writes the four compare/*.json files to outputs/compare/', async () => { + setupReport('current', [trend('TR-100', 'Same', 'Cat', ['v1'], 1_000_000)]); + setupReport('prior', [trend('TR-001', 'Same', 'Cat', ['v1'], 1_000_000)]); + + await runMomCompare('current', 'prior'); + + const outDir = join(FIXTURE_ROOT, 'current', 'outputs', 'compare'); + expect(existsSync(join(outDir, 'new_trends.json'))).toBe(true); + expect(existsSync(join(outDir, 'returning_trends.json'))).toBe(true); + expect(existsSync(join(outDir, 'faded_trends.json'))).toBe(true); + expect(existsSync(join(outDir, 'category_momentum.json'))).toBe(true); + + const ret = JSON.parse(readFileSync(join(outDir, 'returning_trends.json'), 'utf-8')); + expect(ret).toHaveLength(1); + }); +}); diff --git a/v2/pipeline/__tests__/recipes.test.ts b/v2/pipeline/__tests__/recipes.test.ts new file mode 100644 index 0000000..d627928 --- /dev/null +++ b/v2/pipeline/__tests__/recipes.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from 'vitest'; +import { matchRecipe, parseFilterExpression, applyFilter, RECIPES } from '../lib/recipes.js'; +import type { Pass1Video } from '../stages/stage_2_pass1_scrape.js'; + +const fakeVid = (id: string, plays: number, likes: number, saves: number, comments: number, stl_pct: number, daysAgo = 5): Pass1Video => ({ + id, handle: 'creator', url_canonical: `https://www.tiktok.com/@creator/video/${id}`, + caption: '', hashtags: [], plays, likes, saves, comments_count: comments, shares: 0, + stl_pct, duration_sec: 30, + posted_at: new Date(Date.now() - daysAgo * 86400 * 1000).toISOString(), + cover: null, download_url: null, _source: 'test', _scraped_at: new Date().toISOString(), +}); + +describe('matchRecipe', () => { + it('hooks-related questions → A', () => { + expect(matchRecipe('What hooks stop the scroll for our audience?')).toBe('A'); + expect(matchRecipe('How do creators use the first three seconds in beauty?')).toBe('A'); + }); + it('cultural-moment questions → B', () => { + expect(matchRecipe('Why is hair washing emerging as a cultural moment?')).toBe('B'); + expect(matchRecipe('What is shifting in the everything-shower trend?')).toBe('B'); + }); + it('competitor questions → C', () => { + expect(matchRecipe('How does Dove position vs Olay in haircare?')).toBe('C'); + }); + it('audience sentiment questions → D', () => { + expect(matchRecipe('What do users actually feel about scalp products?')).toBe('D'); + }); + it('falls back to B', () => { + expect(matchRecipe('Tell me about beauty content please please please now')).toBe('B'); + }); + it('every recipe id is reachable', () => { + expect(Object.keys(RECIPES).sort()).toEqual(['A', 'B', 'C', 'D']); + }); +}); + +describe('parseFilterExpression + applyFilter', () => { + const videos: Pass1Video[] = [ + fakeVid('111111111111111111', 1_000_000, 100_000, 10_000, 5000, 11.5), + fakeVid('222222222222222222', 500_000, 50_000, 3_000, 2000, 9.0), + fakeVid('333333333333333333', 50_000, 5_000, 1_500, 500, 7.0), + fakeVid('444444444444444444', 100_000, 8_000, 7_000, 1500, 16.5), + ]; + + it('top_by_plays:2', () => { + const f = parseFilterExpression('top_by_plays:2'); + const ids = applyFilter(videos, f).sort(); + expect(ids).toEqual(['111111111111111111', '222222222222222222'].sort()); + }); + + it('AND intersects', () => { + // top_by_plays:3 = vids 1,2,4 (top 3 by plays). top_by_stl:3 = vids 4,1,2 (highest STL with ≥10k plays). + // Intersection = 1,2,4. + const f = parseFilterExpression('top_by_plays:3 AND top_by_stl:3'); + const ids = applyFilter(videos, f).sort(); + expect(ids).toEqual(['111111111111111111', '222222222222222222', '444444444444444444'].sort()); + }); + + it('OR unions', () => { + const f = parseFilterExpression('top_by_plays:2 OR top_by_saves:1'); + const ids = applyFilter(videos, f).sort(); + // top_by_plays:2 = {1,2}; top_by_saves:1 = {1} (video 1 has 10k saves > video 4's 7k). + // Union = {1,2}. + expect(ids).toEqual(['111111111111111111', '222222222222222222'].sort()); + }); + + it('parens force grouping', () => { + const f = parseFilterExpression('(top_by_plays:3 AND top_by_stl:2) OR top_by_saves:1'); + const ids = applyFilter(videos, f); + expect(ids.length).toBeGreaterThan(0); + }); + + it('manual_ids passes through', () => { + const f = parseFilterExpression('manual_ids:111111111111111111,999999999999999999'); + const ids = applyFilter(videos, f).sort(); + expect(ids).toContain('111111111111111111'); + expect(ids).toContain('999999999999999999'); + }); + + it('throws on unknown primitive', () => { + expect(() => parseFilterExpression('top_by_unicorn:5')).toThrow(); + }); + + it('throws on missing close paren', () => { + expect(() => parseFilterExpression('(top_by_plays:5')).toThrow(); + }); +}); diff --git a/v2/pipeline/cli.ts b/v2/pipeline/cli.ts new file mode 100644 index 0000000..0dba24f --- /dev/null +++ b/v2/pipeline/cli.ts @@ -0,0 +1,263 @@ +#!/usr/bin/env tsx +// V2 pipeline CLI. Mirrors brief §11 verbatim: +// pnpm pipe brief|seed|scrape1|select|scrape2|validate|analyse|insights|trends|qa|build|deploy --report +// Each subcommand loads inputs from DB + disk, runs its stage, writes outputs to +// briefs/{report_id}/. Idempotent + resumable via .state/stage{N}.done sentinels. +import { writeFileSync, existsSync } from 'node:fs'; +import { sql } from '../server/db/client.js'; +import { getBriefById } from '../server/db/briefs.js'; +import { BRIEF_INPUT } from '../server/schemas/brief.js'; +import { runStage1Seeds } from './stages/stage_1_seeds.js'; +import { runStage2Pass1Scrape } from './stages/stage_2_pass1_scrape.js'; +import { runStage3Select } from './stages/stage_3_select.js'; +import { runStage4Pass2Enrich } from './stages/stage_4_pass2_enrich.js'; +import { backfillCoversFromRawDumps } from './lib/backfill_covers.js'; +import { runStage5Manifest } from './stages/stage_5_manifest.js'; +import { runStage6Analyse } from './stages/stage_6_analyse.js'; +import { runStage7AtomicInsights } from './stages/stage_7_atomic_insights.js'; +import { runStage8Trends } from './stages/stage_8_trends.js'; +import { runStage9Qa } from './stages/stage_9_qa.js'; +import { runStage10Build } from './stages/stage_10_build.js'; +import { runMomCompare } from './lib/mom_compare.js'; +import { PATHS, ensureDir } from './lib/paths.js'; +import { onClaudeUsage } from './lib/claude.js'; +import { onApifyCost } from './lib/apify_client.js'; +import { onDriftEvent } from './lib/drift_log.js'; +import type { RecipeId } from './lib/recipes.js'; + +interface Args { + command: string; + reportId: string | null; + flags: Record; +} + +function parseArgs(argv: string[]): Args { + const [, , command = '', ...rest] = argv; + const flags: Record = {}; + for (let i = 0; i < rest.length; i++) { + const tok = rest[i]!; + if (!tok.startsWith('--')) continue; + const key = tok.slice(2); + const next = rest[i + 1]; + if (next && !next.startsWith('--')) { flags[key] = next; i++; } + else flags[key] = true; + } + const reportId = (typeof flags.report === 'string' ? flags.report : null); + return { command, reportId, flags }; +} + +function usage(): never { + console.error(`Usage: pnpm pipe --report [flags] + +Commands: + seed Stage 1 — expand the brief into hashtag tiers + search terms (writes seeds.json) + scrape1 Stage 2 — broad Apify pull, hashtag floor applied (TODO Phase C) + select Stage 3 — recipe-led selection (TODO Phase C) + scrape2 Stage 4 — deep enrichment per video (TODO Phase D) + validate Stage 5 — manifest gate (TODO Phase D) + analyse Stage 6 — per-video Claude analysis (TODO Phase E) + insights Stage 7 — atomic insight extraction (TODO Phase E) + trends Stage 8 — trend synthesis (TODO Phase E) + qa Stage 9 — paid/organic + coverage gates (TODO Phase F) + build Stage 10 — dashboard + claude.ai HTML bundle (TODO Phase F) + +Flags: + --report UUID of the brief in the briefs table + --force Invalidate stage sentinels and rerun +`); + process.exit(1); +} + +async function loadBrief(reportId: string): Promise<{ briefRow: NonNullable>>; brief: ReturnType }> { + const briefRow = await getBriefById(reportId); + if (!briefRow) throw new Error(`Brief not found: ${reportId}`); + const parsed = BRIEF_INPUT.parse(briefRow.brief_yaml); + return { briefRow, brief: parsed }; +} + +async function loadBriefAndRow(reportId: string): ReturnType { + return loadBrief(reportId); +} + +function logCost(): void { + let claudeTotal = 0; + let apifyTotal = 0; + onClaudeUsage((u, label) => { + claudeTotal += u.cost_usd; + console.log(`[claude] ${label}: ${u.input_tokens} in / ${u.output_tokens} out / $${u.cost_usd.toFixed(4)} (running: $${claudeTotal.toFixed(4)})`); + }); + onApifyCost((e) => { + apifyTotal += e.cost_usd; + console.log(`[apify] ${e.label}: $${e.cost_usd.toFixed(4)} (running: $${apifyTotal.toFixed(4)})`); + }); + onDriftEvent((d) => { + console.warn(`[drift] ${d.actor} ${d.reason}: id=${d.extracted_id ?? '?'}`); + }); +} + +function writeStageDone(reportId: string, n: number, payload: Record): void { + ensureDir(PATHS.stateDir(reportId)); + writeFileSync(PATHS.stageDone(reportId, n), JSON.stringify(payload, null, 2)); +} + +function isStageDone(reportId: string, n: number, force = false): boolean { + if (force) return false; + return existsSync(PATHS.stageDone(reportId, n)); +} + +async function main(): Promise { + const { command, reportId, flags } = parseArgs(process.argv); + if (!command) usage(); + if (!reportId) { console.error('Missing --report '); usage(); } + + const force = !!flags.force; + logCost(); + + switch (command) { + case 'seed': { + if (isStageDone(reportId, 1, force)) { + console.log(`[stage 1] already done; pass --force to rerun. Output: ${PATHS.seedsJson(reportId)}`); + break; + } + const { brief } = await loadBrief(reportId); + const result = await runStage1Seeds({ reportId, brief }); + writeStageDone(reportId, 1, { command, at: new Date().toISOString(), outputs: result.outputs }); + console.log(`[stage 1] OK — seeds → ${result.outputs.seeds}`); + break; + } + case 'brief': { + const { brief } = await loadBrief(reportId); + const path = PATHS.briefJson(reportId); + writeFileSync(path, JSON.stringify(brief, null, 2)); + console.log(`[brief] dumped → ${path}`); + break; + } + case 'backfill-covers': { + const result = backfillCoversFromRawDumps(reportId); + console.log(`[backfill] patched ${result.patched} of ${result.total} pass1 records with cover/mp4 URLs`); + break; + } + case 'scrape1': { + if (isStageDone(reportId, 2, force)) { + console.log(`[stage 2] already done; pass --force to rerun. Output: ${PATHS.pass1Videos(reportId)}`); + break; + } + const { brief } = await loadBrief(reportId); + const result = await runStage2Pass1Scrape({ reportId, brief }); + writeStageDone(reportId, 2, { command, at: new Date().toISOString(), outputs: result.outputs, total_videos: result.total_videos, total_cost_usd: result.total_cost_usd }); + console.log(`[stage 2] OK — ${result.total_videos} videos, $${result.total_cost_usd.toFixed(2)}`); + break; + } + case 'select': { + if (isStageDone(reportId, 3, force)) { + console.log(`[stage 3] already done; pass --force to rerun. Output: ${PATHS.selectedIds(reportId)}`); + break; + } + const { brief } = await loadBrief(reportId); + const recipe = (typeof flags.recipe === 'string' ? flags.recipe.toUpperCase() : undefined) as RecipeId | undefined; + const customFilter = typeof flags.custom === 'string' ? flags.custom : undefined; + const argsObj: Parameters[0] = { reportId, brief }; + if (recipe) argsObj.forceRecipe = recipe; + if (customFilter) argsObj.customFilter = customFilter; + const result = await runStage3Select(argsObj); + writeStageDone(reportId, 3, { command, at: new Date().toISOString(), outputs: result.outputs, rules: result.rules }); + console.log(`[stage 3] OK — ${result.selected.length} selected, recipe=${result.rules.recipe_id}`); + break; + } + case 'scrape2': { + if (isStageDone(reportId, 4, force)) { + console.log(`[stage 4] already done; pass --force to rerun. Output: ${PATHS.enriched(reportId)}`); + break; + } + const { brief } = await loadBrief(reportId); + const result = await runStage4Pass2Enrich({ reportId, brief }); + writeStageDone(reportId, 4, { command, at: new Date().toISOString(), outputs: result.outputs, total_attempted: result.total_attempted, total_bundled: result.total_bundled, total_dropped: result.total_dropped, drift_events: result.drift_events }); + console.log(`[stage 4] OK — bundled=${result.total_bundled} dropped=${result.total_dropped} drift=${result.drift_events}`); + break; + } + case 'validate': { + const dropFailing = !!flags['drop-failing']; + const { brief } = await loadBrief(reportId); + const result = await runStage5Manifest({ reportId, brief, dropFailing }); + writeStageDone(reportId, 5, { command, at: new Date().toISOString(), passed: result.passed, coverage_pct: result.manifest.summary.coverage_pct, backfill_rounds: result.backfill_rounds }); + if (!result.passed) { + console.error(`[stage 5] FAIL — coverage ${result.manifest.summary.coverage_pct}% (${result.manifest.summary.all_ok}/${result.manifest.selected_count}). Missing per video printed in manifest.json.`); + process.exit(3); + } + console.log(`[stage 5] PASS — coverage 100%`); + break; + } + case 'analyse': { + if (isStageDone(reportId, 6, force)) { + console.log(`[stage 6] already done; pass --force to rerun.`); + break; + } + const result = await runStage6Analyse(reportId); + writeStageDone(reportId, 6, { command, at: new Date().toISOString(), total: result.total, cached: result.cached, fresh: result.fresh }); + console.log(`[stage 6] OK — ${result.total} analyses (${result.cached} cached, ${result.fresh} fresh)`); + break; + } + case 'insights': { + if (isStageDone(reportId, 7, force)) { + console.log(`[stage 7] already done; pass --force to rerun.`); + break; + } + const result = await runStage7AtomicInsights(reportId); + writeStageDone(reportId, 7, { command, at: new Date().toISOString(), total_insights: result.total_insights, by_type: result.by_type }); + console.log(`[stage 7] OK — ${result.total_insights} atomic insights (hook=${result.by_type.hook} visual=${result.by_type.visual} audio=${result.by_type.audio} narrative=${result.by_type.narrative})`); + break; + } + case 'trends': { + if (isStageDone(reportId, 8, force)) { + console.log(`[stage 8] already done; pass --force to rerun.`); + break; + } + const { brief } = await loadBrief(reportId); + const result = await runStage8Trends(reportId, brief); + writeStageDone(reportId, 8, { command, at: new Date().toISOString(), total_trends: result.total_trends, core_trends: result.core_trends, peripheral_trends: result.peripheral_trends, dropped_trends: result.dropped_trends }); + console.log(`[stage 8] OK — ${result.total_trends} trends across ${result.categories.length} categories`); + break; + } + case 'qa': { + if (isStageDone(reportId, 9, force)) { + console.log(`[stage 9] already done; pass --force to rerun.`); + break; + } + const result = await runStage9Qa(reportId); + writeStageDone(reportId, 9, { command, at: new Date().toISOString(), paid_creators: result.paid_creators, mixed_creators: result.mixed_creators, coverage_pct: result.coverage_pct }); + console.log(`[stage 9] OK — paid=${result.paid_creators} mixed=${result.mixed_creators} coverage=${result.coverage_pct}%`); + break; + } + case 'build': { + const target = typeof flags.target === 'string' ? flags.target : 'all'; + const { brief, briefRow } = await loadBriefAndRow(reportId); + + if (target === 'all' || target === 'compare') { + if (briefRow.prior_report_id) { + console.log(`[mom] running compare against prior_report_id=${briefRow.prior_report_id}`); + await runMomCompare(reportId, briefRow.prior_report_id); + } else if (target === 'compare') { + console.error('[mom] target=compare but brief has no prior_report_id; refusing per §16'); + process.exit(4); + } + } + + if (target === 'all' || target === 'dashboard' || target === 'html') { + const result = await runStage10Build(reportId, brief); + writeStageDone(reportId, 10, { command, at: new Date().toISOString(), trend_count: result.trend_count, html_size_bytes: result.html_size_bytes }); + console.log(`[stage 10] OK — dataset=${(result.dataset_size_bytes / 1024).toFixed(1)} KB, html=${(result.html_size_bytes / 1024).toFixed(1)} KB, trends=${result.trend_count}`); + } + break; + } + default: + console.error(`Unknown command: ${command}`); + usage(); + } + + await sql.end({ timeout: 1 }); +} + +main().catch((err) => { + console.error('[pipe] error:', err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/v2/pipeline/lib/apify_client.ts b/v2/pipeline/lib/apify_client.ts new file mode 100644 index 0000000..4ee593b --- /dev/null +++ b/v2/pipeline/lib/apify_client.ts @@ -0,0 +1,141 @@ +// Apify wrapper for V2. +// Adapted from V1 agents/social-listening/apify.ts. +// Changes vs V1: +// - V2 is TikTok-only (Instagram/YouTube/Twitter actors removed). +// - Cost callback signature switched from (costUsd, label, runId) to a structured event. +// - Soft-cap pattern preserved (V1 stage3-discovery-scrape.ts:199-232). +// - Returns raw items; id normalisation happens in the caller via extractTikTokId. +import { envStr, envBool } from '../../server/lib/env.js'; + +export const ACTORS = { + TIKTOK_HASHTAG: 'GdWCkxBtKWOsKjdch', + TIKTOK_PROFILE: 'OtzYfK1ndEGdwWFKQ', + TIKTOK_COMMENTS: 'BDec00yAmCm1QbMEI', + TIKTOK_TRANSCRIPTS:'emQXBCL3xePZYgJyn', +} as const; + +const APIFY_BASE = 'https://api.apify.com/v2'; + +const APIFY_TOKEN = envStr('APIFY_TOKEN') || envStr('APIFY_API_TOKEN'); +const IS_LIVE = envBool('APIFY_LIVE_APPROVED', false); +const IS_TEST = envBool('TEST_MODE', false); + +export function isLive(): boolean { return IS_LIVE; } +export function isTest(): boolean { return IS_TEST; } + +// ─── Budget tracking (per-pipeline, reset between reports) ───────────── +let _running = 0; +let _hardCeiling: number = Number.POSITIVE_INFINITY; +let _softCap: number | null = null; + +export function resetBudget(opts: { hardCeilingUsd: number }): void { + _running = 0; + _softCap = null; + _hardCeiling = opts.hardCeilingUsd; +} +export function getRunningCost(): number { return _running; } +export function setSoftCap(cap: number | null): void { _softCap = cap; } +export function getSoftCap(): number | null { return _softCap; } +export function isBudgetExceeded(): boolean { + if (_softCap !== null && _running >= _softCap) return true; + return _running >= _hardCeiling; +} + +export interface ApifyCostEvent { + cost_usd: number; + label: string; + run_id: string; + dataset_id: string; + actor_id: string; +} +let _onCost: ((e: ApifyCostEvent) => void) | null = null; +export function onApifyCost(cb: (e: ApifyCostEvent) => void): void { _onCost = cb; } + +export interface ApifyRunResult { + items: T[]; + run_id: string; + dataset_id: string; + cost_usd: number; + status: 'OK' | 'BUDGET_SKIP' | 'DRY_RUN'; +} + +export async function runActor( + actorId: string, + input: Record, + label: string, +): Promise> { + if (!IS_LIVE) { + console.log(`[apify dry-run] ${label} actor=${actorId}`); + return { items: [] as T[], run_id: 'dry-run', dataset_id: 'dry-run', cost_usd: 0, status: 'DRY_RUN' }; + } + if (isBudgetExceeded()) { + console.log(`[apify] budget $${_running.toFixed(2)} reached — skipping ${label}`); + return { items: [] as T[], run_id: 'budget-skip', dataset_id: 'budget-skip', cost_usd: 0, status: 'BUDGET_SKIP' }; + } + if (!APIFY_TOKEN) throw new Error('APIFY_TOKEN not set'); + + const startRes = await fetch(`${APIFY_BASE}/acts/${actorId}/runs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${APIFY_TOKEN}` }, + body: JSON.stringify(input), + }); + if (!startRes.ok) { + const errText = await startRes.text(); + throw new Error(`Apify start failed for ${label}: ${startRes.status} ${errText}`); + } + const startData = await startRes.json() as { data: { id: string; defaultDatasetId: string; status: string } }; + const runId = startData.data.id; + const datasetId = startData.data.defaultDatasetId; + console.log(`[apify] ${label} started runId=${runId}`); + + let status = startData.data.status; + let pollCount = 0; + const MAX_POLLS = 120; + while (status !== 'SUCCEEDED' && status !== 'FAILED' && status !== 'ABORTED' && status !== 'TIMED-OUT') { + if (pollCount++ > MAX_POLLS) throw new Error(`Apify ${label} timed out`); + await new Promise((r) => setTimeout(r, 5000)); + try { + const pollRes = await fetch(`${APIFY_BASE}/actor-runs/${runId}`, { + headers: { Authorization: `Bearer ${APIFY_TOKEN}` }, + }); + const pollData = await pollRes.json() as { data: { status: string } }; + status = pollData.data.status; + } catch { /* transient — keep polling */ } + if (pollCount % 6 === 0) console.log(`[apify] ${label} status=${status} (${pollCount * 5}s)`); + } + if (status !== 'SUCCEEDED') throw new Error(`Apify ${label} ended ${status}`); + + let costUsd = 0; + try { + const costRes = await fetch(`${APIFY_BASE}/actor-runs/${runId}`, { + headers: { Authorization: `Bearer ${APIFY_TOKEN}` }, + }); + const costData = await costRes.json() as { data: { usageTotalUsd?: number } }; + costUsd = costData.data.usageTotalUsd || 0; + } catch { /* non-fatal */ } + + const itemsRes = await fetch(`${APIFY_BASE}/datasets/${datasetId}/items?format=json`, { + headers: { Authorization: `Bearer ${APIFY_TOKEN}` }, + }); + let items: T[] = []; + if (itemsRes.ok) { + const ct = itemsRes.headers.get('content-type') || ''; + const text = await itemsRes.text(); + if (ct.includes('json') && text.trim().startsWith('[')) { + try { items = JSON.parse(text) as T[]; } catch { /* fall through to empty */ } + } else { + console.warn(`[apify] ${label} unexpected response (${ct})`); + } + } + _running += costUsd; + console.log(`[apify] ${label} done ${items.length} items $${costUsd.toFixed(4)} (running $${_running.toFixed(2)})`); + if (_onCost) _onCost({ cost_usd: costUsd, label, run_id: runId, dataset_id: datasetId, actor_id: actorId }); + return { items, run_id: runId, dataset_id: datasetId, cost_usd: costUsd, status: 'OK' }; +} + +/** Limits applied by the actor itself (Apify input). Conservative defaults. */ +export function defaultLimits() { + return IS_TEST + ? { resultsPerPage: 50, resultsLimit: 50, maxResults: 50 } + : { resultsPerPage: 200, resultsLimit: 100, maxResults: 200 }; +} diff --git a/v2/pipeline/lib/backfill_covers.ts b/v2/pipeline/lib/backfill_covers.ts new file mode 100644 index 0000000..329069b --- /dev/null +++ b/v2/pipeline/lib/backfill_covers.ts @@ -0,0 +1,68 @@ +// One-shot fix-up: read pass1/raw/*.json dumps and patch cover URLs into existing +// pass1_videos.json + per-video metadata.json. Used when Stage 2's normaliseRaw +// missed a field shape and we don't want to re-spend Apify by re-running Stage 2. +import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { extractTikTokId } from './ids.js'; +import { PATHS } from './paths.js'; +import type { Pass1Video } from '../stages/stage_2_pass1_scrape.js'; + +interface RawAny { + id?: string; + webVideoUrl?: string; + videoMeta?: { coverUrl?: string; originalCoverUrl?: string; downloadAddr?: string }; + mediaUrls?: string[]; + covers?: { default?: string }; +} + +export function backfillCoversFromRawDumps(reportId: string): { patched: number; total: number } { + const rawDir = join(PATHS.pass1(reportId), 'raw'); + const pass1Path = PATHS.pass1Videos(reportId); + if (!existsSync(rawDir) || !existsSync(pass1Path)) { + throw new Error('pass1/raw or pass1_videos.json missing — nothing to backfill'); + } + + // Build id → corrected cover URL map from raw dumps. + const fix = new Map(); + for (const f of readdirSync(rawDir)) { + if (!f.endsWith('.json')) continue; + const items = JSON.parse(readFileSync(join(rawDir, f), 'utf-8')) as RawAny[]; + for (const r of items) { + const id = extractTikTokId(r.id || r.webVideoUrl || ''); + if (!id) continue; + const cover = r.videoMeta?.coverUrl ?? r.videoMeta?.originalCoverUrl ?? r.covers?.default ?? null; + const downloadUrl = r.videoMeta?.downloadAddr ?? r.mediaUrls?.[0] ?? null; + if (!fix.has(id) || (cover && !fix.get(id)?.cover)) fix.set(id, { cover, download_url: downloadUrl }); + } + } + + // Patch pass1_videos.json + const pass1: Pass1Video[] = JSON.parse(readFileSync(pass1Path, 'utf-8')); + let patched = 0; + for (const v of pass1) { + const f = fix.get(v.id); + if (!f) continue; + if (!v.cover && f.cover) { v.cover = f.cover; patched++; } + if (!v.download_url && f.download_url) v.download_url = f.download_url; + } + writeFileSync(pass1Path, JSON.stringify(pass1, null, 2)); + + // Patch per-video metadata.json files (only those that exist already from Stage 4). + const enrichedDir = PATHS.enriched(reportId); + if (existsSync(enrichedDir)) { + for (const id of readdirSync(enrichedDir)) { + if (!statSync(join(enrichedDir, id)).isDirectory()) continue; + const metaPath = join(enrichedDir, id, 'metadata.json'); + if (!existsSync(metaPath)) continue; + const meta: Pass1Video = JSON.parse(readFileSync(metaPath, 'utf-8')); + const f = fix.get(id); + if (!f) continue; + let changed = false; + if (!meta.cover && f.cover) { meta.cover = f.cover; changed = true; } + if (!meta.download_url && f.download_url) { meta.download_url = f.download_url; changed = true; } + if (changed) writeFileSync(metaPath, JSON.stringify(meta, null, 2)); + } + } + + return { patched, total: pass1.length }; +} diff --git a/v2/pipeline/lib/claude.ts b/v2/pipeline/lib/claude.ts new file mode 100644 index 0000000..99f95b0 --- /dev/null +++ b/v2/pipeline/lib/claude.ts @@ -0,0 +1,109 @@ +// Thin Claude API client. Adapted from V1 agents/social-listening/claude-cli.ts:62-285, +// trimmed to what V2 stages need: text + JSON modes, retry-on-invalid-JSON 2x, usage callback. +import { envStr } from '../../server/lib/env.js'; + +const DEFAULT_MODEL = 'claude-opus-4-7'; +const API_BASE = 'https://api.anthropic.com/v1/messages'; + +const PRICING: Record = { + 'claude-opus-4-7': { input: 5, output: 25 }, + 'claude-opus-4-6': { input: 5, output: 25 }, + 'claude-sonnet-4-6': { input: 3, output: 15 }, + 'claude-haiku-4-5': { input: 1, output: 5 }, +}; + +export interface Usage { + input_tokens: number; + output_tokens: number; + cost_usd: number; + model: string; +} + +let onUsageCb: ((u: Usage, label: string) => void) | null = null; +export function onClaudeUsage(cb: (u: Usage, label: string) => void): void { + onUsageCb = cb; +} + +function cost(model: string, ti: number, to: number): number { + const p = PRICING[model] ?? PRICING[DEFAULT_MODEL]!; + return ti * p.input / 1_000_000 + to * p.output / 1_000_000; +} + +interface ApiResponse { + content: Array<{ type: string; text?: string }>; + stop_reason: string; + usage: { input_tokens: number; output_tokens: number }; +} + +async function callRaw(prompt: string, model: string, maxTokens: number): Promise<{ text: string; usage: Usage }> { + const apiKey = envStr('ANTHROPIC_API_KEY'); + if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set'); + const res = await fetch(API_BASE, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + // temperature is deprecated on opus-4-7; the model is deterministic by default. + model, + max_tokens: maxTokens, + messages: [{ role: 'user', content: prompt }], + }), + }); + if (!res.ok) { + const errText = await res.text(); + throw new Error(`Anthropic API error ${res.status}: ${errText}`); + } + const data = await res.json() as ApiResponse; + const text = data.content.filter((b) => b.type === 'text').map((b) => b.text ?? '').join('\n').trim(); + const usage: Usage = { + input_tokens: data.usage.input_tokens, + output_tokens: data.usage.output_tokens, + cost_usd: cost(model, data.usage.input_tokens, data.usage.output_tokens), + model, + }; + return { text, usage }; +} + +export async function callClaude( + prompt: string, + opts: { model?: string; maxTokens?: number; label?: string } = {}, +): Promise { + const model = opts.model ?? DEFAULT_MODEL; + const { text, usage } = await callRaw(prompt, model, opts.maxTokens ?? 16384); + if (onUsageCb) onUsageCb(usage, opts.label ?? 'call'); + return text; +} + +function tryParseJson(text: string): T | null { + const fence = text.match(/```json\s*\n?([\s\S]*?)```/) ?? text.match(/```\s*\n?([\s\S]*?)```/); + const candidates = [ + fence?.[1]?.trim(), + text.match(/(\{[\s\S]*\})/)?.[1], + text.match(/(\[[\s\S]*\])/)?.[1], + text.trim(), + ].filter((x): x is string => !!x); + for (const c of candidates) { + try { return JSON.parse(c) as T; } catch { /* next */ } + } + return null; +} + +export async function callClaudeJSON( + prompt: string, + opts: { model?: string; maxTokens?: number; label?: string } = {}, +): Promise { + const fullPrompt = `${prompt}\n\nCRITICAL: Return ONLY valid JSON. No markdown outside the JSON. No prose before or after.`; + const maxRetries = 2; + let lastErr = ''; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + const raw = await callClaude(fullPrompt, opts); + const parsed = tryParseJson(raw); + if (parsed !== null) return parsed; + lastErr = `attempt ${attempt + 1}: could not parse JSON. First 300 chars: ${raw.slice(0, 300)}`; + console.warn(`[claude] ${lastErr}`); + } + throw new Error(`callClaudeJSON failed after ${maxRetries + 1} attempts. ${lastErr}`); +} diff --git a/v2/pipeline/lib/drift_log.ts b/v2/pipeline/lib/drift_log.ts new file mode 100644 index 0000000..cdf52ac --- /dev/null +++ b/v2/pipeline/lib/drift_log.ts @@ -0,0 +1,54 @@ +// Drift log: when an Apify actor returns a video URL/id we can't match to our +// canonical TikTok id, we record it. This is the V2 commitment that drift is +// LOUD, not silent. Every line in drift_log.jsonl points at a specific actor +// response that needs human review. +import { appendFileSync, mkdirSync, existsSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { PATHS } from './paths.js'; + +export type DriftReason = + | 'no-id-extracted' + | 'id-not-in-selection' + | 'duplicate-id-different-url' + | 'metadata-missing-fields' + | 'date-out-of-window'; + +export interface DriftEntry { + at: string; + actor: string; // human label e.g. "TIKTOK_TRANSCRIPTS" + reason: DriftReason; + source_url: string | null; + extracted_id: string | null; + context?: Record; +} + +let driftCounter = 0; +let onDrift: ((entry: DriftEntry) => void) | null = null; + +export function onDriftEvent(cb: (entry: DriftEntry) => void): void { + onDrift = cb; +} + +export function getDriftCount(): number { + return driftCounter; +} + +export function resetDriftCounter(): void { + driftCounter = 0; +} + +/** Truncate the drift log file at start of a fresh Stage 4 run. */ +export function clearDriftLog(reportId: string): void { + const path = PATHS.driftLog(reportId); + if (existsSync(path)) writeFileSync(path, ''); +} + +export function logDrift(reportId: string, entry: Omit): void { + const full: DriftEntry = { at: new Date().toISOString(), ...entry }; + const path = PATHS.driftLog(reportId); + if (!existsSync(dirname(path))) mkdirSync(dirname(path), { recursive: true }); + appendFileSync(path, JSON.stringify(full) + '\n'); + driftCounter++; + if (onDrift) onDrift(full); + console.warn(`[drift] ${entry.actor} ${entry.reason}: id=${entry.extracted_id ?? '(none)'} url=${entry.source_url ?? '(none)'}`); +} diff --git a/v2/pipeline/lib/engagement_floor.ts b/v2/pipeline/lib/engagement_floor.ts new file mode 100644 index 0000000..745f48c --- /dev/null +++ b/v2/pipeline/lib/engagement_floor.ts @@ -0,0 +1,60 @@ +// The V2 quality knob — drops Apify items below a per-brief engagement threshold. +// Applied AFTER actors return (Apify-side filtering is best-effort: TikTok hashtag +// scraper accepts `minPlayCount` but most others don't, so we always re-validate +// locally for correctness). + +export interface EngagementFloor { + min_likes: number; + min_plays: number; + min_stl_pct: number; +} + +export interface EngagementCounters { + raw_returned: number; + dropped_min_likes: number; + dropped_min_plays: number; + dropped_min_stl: number; + dropped_zero_plays: number; + kept_after_floor: number; +} + +export interface FloorableItem { + plays: number; + likes: number; + saves?: number; + comments_count?: number; + shares?: number; +} + +export function computeStlPct(item: FloorableItem): number { + if (!item.plays || item.plays <= 0) return 0; + const stl = (item.likes ?? 0) + (item.saves ?? 0) + (item.comments_count ?? 0) + (item.shares ?? 0); + return (stl / item.plays) * 100; +} + +export function applyEngagementFloor( + items: T[], + floor: EngagementFloor, +): { kept: T[]; counters: EngagementCounters } { + const c: EngagementCounters = { + raw_returned: items.length, + dropped_min_likes: 0, + dropped_min_plays: 0, + dropped_min_stl: 0, + dropped_zero_plays: 0, + kept_after_floor: 0, + }; + const kept: T[] = []; + for (const it of items) { + if (!it.plays || it.plays <= 0) { c.dropped_zero_plays++; continue; } + if (it.plays < floor.min_plays) { c.dropped_min_plays++; continue; } + if (it.likes < floor.min_likes) { c.dropped_min_likes++; continue; } + if (floor.min_stl_pct > 0) { + const stl = computeStlPct(it); + if (stl < floor.min_stl_pct) { c.dropped_min_stl++; continue; } + } + kept.push(it); + } + c.kept_after_floor = kept.length; + return { kept, counters: c }; +} diff --git a/v2/pipeline/lib/frames.ts b/v2/pipeline/lib/frames.ts new file mode 100644 index 0000000..6b410fb --- /dev/null +++ b/v2/pipeline/lib/frames.ts @@ -0,0 +1,56 @@ +// ffmpeg-based frame extraction. Cap based on video length per V3 brief §4 stage 4: +// ≤15s : 1 fps (max 15) +// 16–60s: 1/2 fps (max 30) +// 61–180s: 1/4 fps (max 45) +// >180s : 1/6 fps (max 60) +// All frames downscaled to 720px wide jpg. +import { spawnSync } from 'node:child_process'; +import { mkdirSync, existsSync, readdirSync } from 'node:fs'; + +export interface FrameExtractOpts { + /** path to local mp4 file. */ + mp4Path: string; + /** output directory; will be created. Files written as 0001.jpg, 0002.jpg, … */ + outDir: string; + /** video duration in seconds (used to pick fps + cap). */ + durationSec: number; + /** override fps and cap (testing only). */ + override?: { fps?: number; cap?: number }; +} + +export interface FrameExtractResult { + ok: boolean; + frames: string[]; // basenames written + fps: number; + cap: number; + error?: string; +} + +export function chooseFpsAndCap(durationSec: number): { fps: number; cap: number } { + if (durationSec <= 15) return { fps: 1, cap: 15 }; + if (durationSec <= 60) return { fps: 0.5, cap: 30 }; + if (durationSec <= 180) return { fps: 0.25, cap: 45 }; + return { fps: 1 / 6, cap: 60 }; +} + +export function extractFrames(opts: FrameExtractOpts): FrameExtractResult { + if (!existsSync(opts.mp4Path)) return { ok: false, frames: [], fps: 0, cap: 0, error: 'mp4 missing' }; + mkdirSync(opts.outDir, { recursive: true }); + const { fps, cap } = opts.override + ? { fps: opts.override.fps ?? 1, cap: opts.override.cap ?? 15 } + : chooseFpsAndCap(opts.durationSec); + + const args = [ + '-y', '-i', opts.mp4Path, + '-vf', `fps=${fps},scale=720:-2`, + '-frames:v', String(cap), + '-q:v', '4', + `${opts.outDir}/%04d.jpg`, + ]; + const res = spawnSync('ffmpeg', args, { encoding: 'utf-8' }); + if (res.status !== 0) { + return { ok: false, frames: [], fps, cap, error: `ffmpeg exit ${res.status}: ${res.stderr.slice(-400)}` }; + } + const files = existsSync(opts.outDir) ? readdirSync(opts.outDir).filter((f) => f.endsWith('.jpg')).sort() : []; + return { ok: files.length > 0, frames: files, fps, cap }; +} diff --git a/v2/pipeline/lib/ids.ts b/v2/pipeline/lib/ids.ts new file mode 100644 index 0000000..0911731 --- /dev/null +++ b/v2/pipeline/lib/ids.ts @@ -0,0 +1,111 @@ +// THE LINKING FIX — single canonical TikTok-video-id extractor. +// +// V1 joined per-video assets via `Map.get(url)` against URLs returned by *different* +// Apify actors. The actors return slightly different URL shapes (with/without `www`, +// with/without trailing query params, `vm.tiktok.com` shortlinks). Any drift silently +// produced `undefined` and dropped the asset, so trends ended up citing the wrong +// videos. +// +// V2 collapses every URL form to the 19-digit TikTok numeric id at scrape-normalise +// time. That id is the row PK in `videos`, the folder name in `enriched/{id}/`, and +// the join key for every Apify response. URL is presentation, not key. +// +// This module is the ONLY place URL→id conversion happens. If a URL form makes it +// past Stage 2 without a numeric id, it is logged to drift_log.jsonl and the asset +// lands as `failed` in the manifest. It does not silently drop. + +const ID_RX = /^\d{15,21}$/; + +/** + * Accepts any TikTok URL form, returns the 19-ish-digit numeric id, or null. + * + * Handled forms (see ids.test.ts for the full fixture): + * - https://www.tiktok.com/@handle/video/7280000000000000000 + * - https://www.tiktok.com/@handle/video/7280000000000000000?is_from_webapp=1 + * - https://tiktok.com/@handle/video/7280000000000000000 + * - https://m.tiktok.com/v/7280000000000000000.html + * - https://www.tiktok.com/t/7280000000000000000 (older share link) + * - 7280000000000000000 (raw id) + * + * NOT handled (returns null, callers should resolve before calling): + * - https://vm.tiktok.com/ZMabc123/ (short link — needs HEAD resolve) + * - https://vt.tiktok.com/ZSabc123/ (regional short link) + * + * For short links, use `resolveShortLink(url)` first to get the redirect target, + * then pass that to `extractTikTokId`. + */ +export function extractTikTokId(input: unknown): string | null { + if (input === null || input === undefined) return null; + const s = String(input).trim(); + if (!s) return null; + + // Raw numeric id. + if (ID_RX.test(s)) return s; + + // Anything with a /video/ segment. + const videoMatch = s.match(/\/video\/(\d{15,21})\b/); + if (videoMatch?.[1]) return videoMatch[1]; + + // Older share form: /v/.html + const vMatch = s.match(/\/v\/(\d{15,21})\b/); + if (vMatch?.[1]) return vMatch[1]; + + // Older share form: /t/ + const tMatch = s.match(/\/t\/(\d{15,21})\b/); + if (tMatch?.[1]) return tMatch[1]; + + // Last-ditch: any 15–21 digit run that is NOT inside a millisecond timestamp + // (e.g. tiktok URLs sometimes carry `lang=en&...&_t=8abc1234567`). + // Only accept if surrounded by URL/JSON-ish boundaries. + const fallback = s.match(/(?:^|[^\d])(\d{17,20})(?:$|[^\d])/); + if (fallback?.[1] && ID_RX.test(fallback[1])) return fallback[1]; + + return null; +} + +/** + * Build a canonical URL from an id + handle. We never store input URLs in the + * `videos` table — only the canonical form, derived from the id. + */ +export function canonicalTikTokUrl(id: string, handle: string): string { + const cleanHandle = handle.replace(/^@/, ''); + return `https://www.tiktok.com/@${cleanHandle}/video/${id}`; +} + +/** + * Resolve a TikTok short link (vm.tiktok.com / vt.tiktok.com) to its full URL. + * Returns null on network failure or non-redirect response. Callers must pass the + * resolved URL back through `extractTikTokId` to get the numeric id. + * + * NOTE: Apify's hashtag scraper already returns full URLs in `webVideoUrl` for the + * cases we care about, so this is rarely hit. Kept for defence in depth. + */ +export async function resolveShortLink(shortUrl: string): Promise { + try { + const res = await fetch(shortUrl, { method: 'HEAD', redirect: 'manual' }); + const loc = res.headers.get('location'); + if (!loc) return null; + return loc; + } catch { + return null; + } +} + +export interface DriftEvent { + actor: string; + reason: 'no-id-extracted' | 'id-not-in-selection' | 'duplicate-id-different-url'; + source_url: string | null; + extracted_id: string | null; + context?: Record; + at: string; +} + +/** + * Test helper: read every URL form V1 has seen drift in. Loaded from a + * fixture file so contributors can add new mutation forms without touching code. + */ +export const DRIFT_REASONS = [ + 'no-id-extracted', + 'id-not-in-selection', + 'duplicate-id-different-url', +] as const; diff --git a/v2/pipeline/lib/manifest.ts b/v2/pipeline/lib/manifest.ts new file mode 100644 index 0000000..2add3a6 --- /dev/null +++ b/v2/pipeline/lib/manifest.ts @@ -0,0 +1,208 @@ +// Manifest validation. Hard gate: refuses to advance unless every selected +// video has every required asset, content-validated. +// +// Validity rules (V3 brief §4 stage 5): +// - metadata.ok : full record present in bundle.json +// - transcript.ok : non-empty text_en +// - comments.ok : ≥5 comments, all with text_en +// - frames.ok : ≥3 frames extracted (the brief says ≥3) +// - cover.ok : local cover.jpg present and >5 KB +import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { PATHS } from './paths.js'; +import type { VideoBundle } from '../stages/stage_4_pass2_enrich.js'; + +export const ASSET_KINDS = ['metadata', 'cover', 'transcript', 'comments', 'frames', 'bundle'] as const; +export type AssetKind = typeof ASSET_KINDS[number]; + +const BUNDLE_VALIDATION = z.object({ + id: z.string(), + metadata: z.unknown(), + transcript: z.union([z.null(), z.object({ text_en: z.string().min(1) })]), + comments: z.array(z.object({ text_en: z.string().min(1) })), + frames: z.array(z.object({ index: z.number(), path: z.string() })), + cover_local: z.union([z.null(), z.string()]), + _validation: z.object({ all_ok: z.boolean(), missing: z.array(z.string()) }), +}); + +export interface ManifestVideoEntry { + id: string; + metadata: { ok: boolean; path?: string; error?: string }; + cover: { ok: boolean; path?: string; error?: string }; + transcript: { ok: boolean; path?: string; language_detected?: string; error?: string }; + comments: { ok: boolean; path?: string; count?: number; error?: string }; + frames: { ok: boolean; path?: string; count?: number; error?: string }; + bundle: { ok: boolean; path?: string; error?: string }; + all_ok: boolean; + missing: AssetKind[]; +} + +export interface Manifest { + report_id: string; + selected_count: number; + summary: { + metadata_ok: number; + transcript_ok: number; + comments_ok: number; + frames_ok: number; + cover_ok: number; + bundle_ok: number; + all_ok: number; + coverage_pct: number; + }; + videos: ManifestVideoEntry[]; + built_at: string; +} + +export class HardGateError extends Error { + manifest: Manifest; + constructor(message: string, manifest: Manifest) { + super(message); + this.name = 'HardGateError'; + this.manifest = manifest; + } +} + +function validateOne(reportId: string, id: string): ManifestVideoEntry { + const dir = PATHS.enrichedVideo(reportId, id); + const entry: ManifestVideoEntry = { + id, + metadata: { ok: false }, + cover: { ok: false }, + transcript: { ok: false }, + comments: { ok: false }, + frames: { ok: false }, + bundle: { ok: false }, + all_ok: false, + missing: [], + }; + + // metadata.json + const metaPath = join(dir, 'metadata.json'); + if (existsSync(metaPath)) { + try { + JSON.parse(readFileSync(metaPath, 'utf-8')); + entry.metadata = { ok: true, path: metaPath }; + } catch (e) { + entry.metadata = { ok: false, error: `parse: ${(e as Error).message}` }; + } + } else entry.metadata = { ok: false, error: 'missing' }; + + // cover.jpg + const coverPath = join(dir, 'cover.jpg'); + if (existsSync(coverPath)) { + const sz = statSync(coverPath).size; + if (sz > 5_000) entry.cover = { ok: true, path: coverPath }; + else entry.cover = { ok: false, error: `too small (${sz} bytes)` }; + } else entry.cover = { ok: false, error: 'missing' }; + + // transcript.json + const tPath = join(dir, 'transcript.json'); + if (existsSync(tPath)) { + try { + const t = JSON.parse(readFileSync(tPath, 'utf-8')) as { text_en?: string; language_detected?: string }; + if (t.text_en && t.text_en.trim().length > 0) { + const tEntry: { ok: true; path: string; language_detected?: string } = { ok: true, path: tPath }; + if (t.language_detected) tEntry.language_detected = t.language_detected; + entry.transcript = tEntry; + } else entry.transcript = { ok: false, error: 'empty text_en' }; + } catch (e) { + entry.transcript = { ok: false, error: `parse: ${(e as Error).message}` }; + } + } else entry.transcript = { ok: false, error: 'missing' }; + + // comments.json + const cPath = join(dir, 'comments.json'); + if (existsSync(cPath)) { + try { + const arr = JSON.parse(readFileSync(cPath, 'utf-8')) as Array<{ text_en?: string }>; + const validCount = arr.filter((c) => typeof c.text_en === 'string' && c.text_en.length > 0).length; + if (validCount >= 5) entry.comments = { ok: true, path: cPath, count: validCount }; + else entry.comments = { ok: false, count: validCount, error: `only ${validCount} comments with text_en` }; + } catch (e) { + entry.comments = { ok: false, error: `parse: ${(e as Error).message}` }; + } + } else entry.comments = { ok: false, error: 'missing' }; + + // frames/ — when MANIFEST_FRAMES_OPTIONAL=true, frames are advisory (mp4 download + // requires shouldDownloadVideos:true on Stage 2 which costs more; many runs skip). + const framesOptional = process.env.MANIFEST_FRAMES_OPTIONAL === 'true'; + const framesDir = join(dir, 'frames'); + if (existsSync(framesDir)) { + try { + const fileCount = readdirSync(framesDir).filter((f) => f.endsWith('.jpg')).length; + if (fileCount >= 3) entry.frames = { ok: true, path: framesDir, count: fileCount }; + else if (framesOptional) entry.frames = { ok: true, path: framesDir, count: fileCount }; + else entry.frames = { ok: false, count: fileCount, error: `only ${fileCount} frames extracted` }; + } catch (e) { + entry.frames = { ok: false, error: (e as Error).message }; + } + } else if (framesOptional) { + entry.frames = { ok: true, count: 0 }; + } else entry.frames = { ok: false, error: 'missing' }; + + // bundle.json — schema check + const bPath = join(dir, 'bundle.json'); + if (existsSync(bPath)) { + try { + const data = JSON.parse(readFileSync(bPath, 'utf-8')); + const parsed = BUNDLE_VALIDATION.safeParse(data); + if (parsed.success) entry.bundle = { ok: true, path: bPath }; + else entry.bundle = { ok: false, error: `schema: ${parsed.error.message.slice(0, 200)}` }; + } catch (e) { + entry.bundle = { ok: false, error: `parse: ${(e as Error).message}` }; + } + } else entry.bundle = { ok: false, error: 'missing' }; + + const missing: AssetKind[] = []; + if (!entry.metadata.ok) missing.push('metadata'); + if (!entry.cover.ok) missing.push('cover'); + if (!entry.transcript.ok) missing.push('transcript'); + if (!entry.comments.ok) missing.push('comments'); + if (!entry.frames.ok) missing.push('frames'); + if (!entry.bundle.ok) missing.push('bundle'); + entry.missing = missing; + entry.all_ok = missing.length === 0; + return entry; +} + +export function buildManifest(reportId: string, ids: string[]): Manifest { + const videos = ids.map((id) => validateOne(reportId, id)); + const summary = { + metadata_ok: videos.filter((v) => v.metadata.ok).length, + transcript_ok: videos.filter((v) => v.transcript.ok).length, + comments_ok: videos.filter((v) => v.comments.ok).length, + frames_ok: videos.filter((v) => v.frames.ok).length, + cover_ok: videos.filter((v) => v.cover.ok).length, + bundle_ok: videos.filter((v) => v.bundle.ok).length, + all_ok: videos.filter((v) => v.all_ok).length, + coverage_pct: videos.length === 0 ? 0 : Math.round((videos.filter((v) => v.all_ok).length / videos.length) * 10000) / 100, + }; + return { + report_id: reportId, + selected_count: ids.length, + summary, + videos, + built_at: new Date().toISOString(), + }; +} + +export function writeManifest(reportId: string, manifest: Manifest): string { + const path = PATHS.manifestJson(reportId); + mkdirSync(path.replace(/[^/]+$/, ''), { recursive: true }); + writeFileSync(path, JSON.stringify(manifest, null, 2)); + return path; +} + +export function loadManifest(reportId: string): Manifest | null { + const p = PATHS.manifestJson(reportId); + if (!existsSync(p)) return null; + return JSON.parse(readFileSync(p, 'utf-8')) as Manifest; +} + +export function loadBundle(reportId: string, videoId: string): VideoBundle | null { + const p = `${PATHS.enrichedVideo(reportId, videoId)}/bundle.json`; + if (!existsSync(p)) return null; + return JSON.parse(readFileSync(p, 'utf-8')) as VideoBundle; +} diff --git a/v2/pipeline/lib/mom_compare.ts b/v2/pipeline/lib/mom_compare.ts new file mode 100644 index 0000000..9296328 --- /dev/null +++ b/v2/pipeline/lib/mom_compare.ts @@ -0,0 +1,160 @@ +// §16 — Month-over-month trend comparison. +// +// Inputs: current report + prior report (both must exist on disk; brief.prior_report_id +// resolves to a prior report's fs_root). Per V3, fails LOUDLY if prior_report_id is set +// but the prior report is missing — never silent-skip. +// +// Algorithm: +// 1. Trend matching: for every current trend, find closest prior trend by +// - editorial-name similarity (cheap Jaro-Winkler-ish) +// - shared video ids (Jaccard) +// - shared category (soft tie-breaker) +// match_score = 0.5 * name + 0.3 * videos + 0.2 * category. Threshold 0.45 = returning. +// 2. Faded: every prior trend with no match above threshold. +// 3. Velocity delta for returning trends. +// 4. Category momentum. +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { PATHS } from './paths.js'; +import type { Trend } from '../stages/stage_8_trends.js'; + +function root(): string { + return process.env.BRIEFS_ROOT || resolve(process.cwd(), 'briefs'); +} + +function loadTrends(reportId: string): Trend[] { + const p = PATHS.trends(reportId); + if (!existsSync(p)) throw new Error(`trends.json missing for ${reportId} at ${p}`); + return JSON.parse(readFileSync(p, 'utf-8')); +} + +function priorReportRootExists(priorReportId: string): boolean { + return existsSync(join(root(), priorReportId, 'trends.json')); +} + +// Cheap normalised string similarity (token Jaccard on words). +function nameSimilarity(a: string, b: string): number { + const tokenise = (s: string) => new Set(s.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter((t) => t.length >= 3)); + const A = tokenise(a); + const B = tokenise(b); + if (A.size === 0 || B.size === 0) return 0; + let inter = 0; + for (const t of A) if (B.has(t)) inter++; + return inter / (A.size + B.size - inter); +} + +function jaccard(a: T[], b: T[]): number { + const A = new Set(a); + const B = new Set(b); + if (A.size === 0 && B.size === 0) return 0; + let inter = 0; + for (const t of A) if (B.has(t)) inter++; + return inter / (A.size + B.size - inter); +} + +const RETURNING_THRESHOLD = 0.45; + +export interface MomResult { + new_trends: Array<{ trend_id: string; name: string; rationale: string }>; + returning_trends: Array<{ + trend_id: string; + prior_trend_id: string; + match_score: number; + velocity_delta: { plays_total_pct: number; video_count: number }; + }>; + faded_trends: Array<{ prior_trend_id: string; name: string }>; + category_momentum: Array<{ + category: string; + new: number; returning: number; faded: number; + plays_delta_pct: number; + label: 'expanding' | 'stable' | 'contracting'; + }>; +} + +export async function runMomCompare(reportId: string, priorReportId: string): Promise<{ ok: true; outputs: Record; result: MomResult }> { + if (!priorReportRootExists(priorReportId)) { + throw new Error(`Prior report '${priorReportId}' not found on disk (expected trends.json at ${join(root(), priorReportId, 'trends.json')}). brief.prior_report_id was set; build fails loudly per §16.`); + } + const current = loadTrends(reportId); + const prior = loadTrends(priorReportId); + + // Match current → prior + const usedPrior = new Set(); + const returning: MomResult['returning_trends'] = []; + const newTrends: MomResult['new_trends'] = []; + + for (const c of current) { + let best: { p: Trend; score: number } | null = null; + for (const p of prior) { + if (usedPrior.has(p.trend_id)) continue; + const score = 0.5 * nameSimilarity(c.name, p.name) + + 0.3 * jaccard(c.supporting_video_ids, p.supporting_video_ids) + + 0.2 * (c.category === p.category ? 1 : 0); + if (!best || score > best.score) best = { p, score }; + } + if (best && best.score >= RETURNING_THRESHOLD) { + usedPrior.add(best.p.trend_id); + const playsPct = best.p.kpis.plays_total > 0 + ? Math.round(((c.kpis.plays_total - best.p.kpis.plays_total) / best.p.kpis.plays_total) * 100) + : 0; + returning.push({ + trend_id: c.trend_id, + prior_trend_id: best.p.trend_id, + match_score: Math.round(best.score * 100) / 100, + velocity_delta: { + plays_total_pct: playsPct, + video_count: c.kpis.videos - best.p.kpis.videos, + }, + }); + } else { + newTrends.push({ trend_id: c.trend_id, name: c.name, rationale: 'no prior match above threshold' }); + } + } + + const faded: MomResult['faded_trends'] = prior + .filter((p) => !usedPrior.has(p.trend_id)) + .map((p) => ({ prior_trend_id: p.trend_id, name: p.name })); + + // Category momentum + const cats = new Set([...current.map((t) => t.category), ...prior.map((t) => t.category)]); + const newSet = new Set(newTrends.map((n) => n.trend_id)); + const returningSet = new Set(returning.map((r) => r.trend_id)); + const fadedPriorSet = new Set(faded.map((f) => f.prior_trend_id)); + const categoryMomentum = [...cats].map((cat) => { + const newCount = current.filter((t) => t.category === cat && newSet.has(t.trend_id)).length; + const retCount = current.filter((t) => t.category === cat && returningSet.has(t.trend_id)).length; + const fadedCount = prior.filter((t) => t.category === cat && fadedPriorSet.has(t.trend_id)).length; + const curPlays = current.filter((t) => t.category === cat).reduce((s, t) => s + t.kpis.plays_total, 0); + const priorPlays = prior.filter((t) => t.category === cat).reduce((s, t) => s + t.kpis.plays_total, 0); + const playsDeltaPct = priorPlays > 0 ? Math.round(((curPlays - priorPlays) / priorPlays) * 100) : 0; + const label: 'expanding' | 'stable' | 'contracting' = + playsDeltaPct > 15 ? 'expanding' : playsDeltaPct < -15 ? 'contracting' : 'stable'; + return { category: cat, new: newCount, returning: retCount, faded: fadedCount, plays_delta_pct: playsDeltaPct, label }; + }).sort((a, b) => b.plays_delta_pct - a.plays_delta_pct); + + const result: MomResult = { + new_trends: newTrends, + returning_trends: returning, + faded_trends: faded, + category_momentum: categoryMomentum, + }; + + // Write outputs to outputs/compare/ + const dir = join(PATHS.outputsDir(reportId), 'compare'); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'new_trends.json'), JSON.stringify(result.new_trends, null, 2)); + writeFileSync(join(dir, 'returning_trends.json'), JSON.stringify(result.returning_trends, null, 2)); + writeFileSync(join(dir, 'faded_trends.json'), JSON.stringify(result.faded_trends, null, 2)); + writeFileSync(join(dir, 'category_momentum.json'), JSON.stringify(result.category_momentum, null, 2)); + + return { + ok: true, + outputs: { + new_trends: join(dir, 'new_trends.json'), + returning_trends: join(dir, 'returning_trends.json'), + faded_trends: join(dir, 'faded_trends.json'), + category_momentum: join(dir, 'category_momentum.json'), + }, + result, + }; +} diff --git a/v2/pipeline/lib/paths.ts b/v2/pipeline/lib/paths.ts new file mode 100644 index 0000000..9525941 --- /dev/null +++ b/v2/pipeline/lib/paths.ts @@ -0,0 +1,51 @@ +// Resolves the on-disk briefs// tree per V3 brief §10. +// Used by every stage so paths stay consistent. +import { mkdirSync, existsSync } from 'node:fs'; +import { resolve, join } from 'node:path'; + +// Dynamic so tests can override per-suite via process.env.BRIEFS_ROOT. +function root(): string { + return process.env.BRIEFS_ROOT || resolve(process.cwd(), 'briefs'); +} + +export function reportRoot(reportId: string): string { + const p = resolve(root(), reportId); + if (!existsSync(p)) mkdirSync(p, { recursive: true }); + return p; +} + +export function reportPath(reportId: string, ...parts: string[]): string { + const p = join(reportRoot(reportId), ...parts); + return p; +} + +export function ensureDir(p: string): string { + if (!existsSync(p)) mkdirSync(p, { recursive: true }); + return p; +} + +export const PATHS = { + briefYaml: (id: string) => reportPath(id, 'brief.yaml'), + briefJson: (id: string) => reportPath(id, 'brief.json'), + seedsJson: (id: string) => reportPath(id, 'seeds.json'), + pass1: (id: string) => reportPath(id, 'pass1'), + pass1Videos: (id: string) => reportPath(id, 'pass1', 'pass1_videos.json'), + spendLog: (id: string) => reportPath(id, 'pass1', 'spend_log.json'), + pass2: (id: string) => reportPath(id, 'pass2'), + selectedIds: (id: string) => reportPath(id, 'pass2', 'selected_video_ids.json'), + selectionRules: (id: string) => reportPath(id, 'pass2', 'selection_rules.json'), + driftLog: (id: string) => reportPath(id, 'pass2', 'drift_log.jsonl'), + enriched: (id: string) => reportPath(id, 'enriched'), + enrichedVideo: (id: string, vid: string) => reportPath(id, 'enriched', vid), + manifestJson: (id: string) => reportPath(id, 'manifest.json'), + analysisDir: (id: string) => reportPath(id, 'analysis'), + atomicInsights: (id: string) => reportPath(id, 'atomic_insights.json'), + trends: (id: string) => reportPath(id, 'trends.json'), + categories: (id: string) => reportPath(id, 'categories.json'), + qaDir: (id: string) => reportPath(id, 'qa'), + outputsDir: (id: string) => reportPath(id, 'outputs'), + datasetV2: (id: string) => reportPath(id, 'outputs', 'dataset_v2.json'), + dashboardHtml: (id: string) => reportPath(id, 'outputs', 'dashboard.html'), + stateDir: (id: string) => reportPath(id, '.state'), + stageDone: (id: string, n: number) => reportPath(id, '.state', `stage${n}.done`), +}; diff --git a/v2/pipeline/lib/recipes.ts b/v2/pipeline/lib/recipes.ts new file mode 100644 index 0000000..dafc8ff --- /dev/null +++ b/v2/pipeline/lib/recipes.ts @@ -0,0 +1,210 @@ +// §4.5b selection recipes + filter primitives. Recipe is matched from the brief's +// business_question by trigger phrases; user can override by passing a recipe label +// or a custom filter expression. +import type { Pass1Video } from '../stages/stage_2_pass1_scrape.js'; + +export type RecipeId = 'A' | 'B' | 'C' | 'D'; + +export interface RecipeDef { + id: RecipeId; + name: string; + triggers: string[]; + default_filter: string; + rationale: string; +} + +export const RECIPES: Record = { + A: { + id: 'A', + name: 'What stops the scroll', + triggers: ['hook', 'stops the scroll', 'first three seconds', 'attention'], + default_filter: 'top_by_stl:80 OR top_by_velocity:40', + rationale: 'STL% is the clearest hook-quality proxy; velocity catches what is catching on right now.', + }, + B: { + id: 'B', + name: 'Why is X having a moment', + triggers: ['cultural moment', 'why is', 'emerging', 'shift', 'trend'], + default_filter: 'top_by_saves:60 AND (top_by_plays:100 OR top_by_comments:50)', + rationale: 'Saves signal personal resonance; plays + comments capture mass and conversation.', + }, + C: { + id: 'C', + name: 'How does X position vs competitors', + triggers: ['competitor', 'positioning', 'market share', ' vs '], + default_filter: 'top_by_plays:80', + rationale: 'Forces the brand and competitor sets in (handles preselected by Stage 2), then adds the cultural top to compare against.', + }, + D: { + id: 'D', + name: 'What do users actually feel about X', + triggers: ['what do users', 'audience feeling', 'reception', 'reaction', 'sentiment'], + default_filter: 'top_by_comments:60 AND top_by_stl:40', + rationale: 'Comments carry the truth; STL% filters out videos no one watched long enough to react to.', + }, +}; + +export function matchRecipe(businessQuestion: string): RecipeId { + const q = businessQuestion.toLowerCase(); + // Order: A → C → D → B (most specific to most general) + for (const id of ['A', 'C', 'D', 'B'] as RecipeId[]) { + const r = RECIPES[id]; + if (r.triggers.some((t) => q.includes(t.toLowerCase()))) return id; + } + return 'B'; +} + +// ─── Filter primitives ───────────────────────────────────────────────── + +type FilterFn = (videos: Pass1Video[]) => Set; + +interface ParsedFilter { + expr: FilterAst; + raw: string; +} + +type FilterAst = + | { kind: 'top_by_plays'; n: number } + | { kind: 'top_by_stl'; n: number; min_plays?: number } + | { kind: 'top_by_comments'; n: number } + | { kind: 'top_by_saves'; n: number } + | { kind: 'top_by_velocity'; n: number; min_age_days?: number } + | { kind: 'manual_ids'; ids: string[] } + | { kind: 'AND'; left: FilterAst; right: FilterAst } + | { kind: 'OR'; left: FilterAst; right: FilterAst }; + +function topNByKey(videos: Pass1Video[], n: number, key: K): Set { + const sorted = [...videos].sort((a, b) => Number(b[key] ?? 0) - Number(a[key] ?? 0)); + return new Set(sorted.slice(0, n).map((v) => v.id)); +} + +function topByVelocity(videos: Pass1Video[], n: number, minAgeDays = 2): Set { + const now = Date.now(); + const scored = videos + .map((v) => { + const ageMs = now - new Date(v.posted_at).getTime(); + const ageDays = ageMs / (1000 * 60 * 60 * 24); + if (ageDays < minAgeDays) return null; + const velocity = v.plays / Math.max(ageDays, 1); + return { id: v.id, velocity }; + }) + .filter((x): x is { id: string; velocity: number } => x !== null) + .sort((a, b) => b.velocity - a.velocity); + return new Set(scored.slice(0, n).map((s) => s.id)); +} + +function evalAst(ast: FilterAst, videos: Pass1Video[]): Set { + switch (ast.kind) { + case 'top_by_plays': return topNByKey(videos, ast.n, 'plays'); + case 'top_by_stl': { + const minP = ast.min_plays ?? 10000; + return topNByKey(videos.filter((v) => v.plays >= minP), ast.n, 'stl_pct'); + } + case 'top_by_comments': return topNByKey(videos, ast.n, 'comments_count'); + case 'top_by_saves': return topNByKey(videos, ast.n, 'saves'); + case 'top_by_velocity': return topByVelocity(videos, ast.n, ast.min_age_days); + case 'manual_ids': return new Set(ast.ids); + case 'AND': { + const l = evalAst(ast.left, videos); + const r = evalAst(ast.right, videos); + return new Set([...l].filter((id) => r.has(id))); + } + case 'OR': { + const l = evalAst(ast.left, videos); + const r = evalAst(ast.right, videos); + return new Set([...l, ...r]); + } + } +} + +// ─── Parser ──────────────────────────────────────────────────────────── + +interface Token { type: 'TOKEN' | 'AND' | 'OR' | 'LPAREN' | 'RPAREN'; value: string } + +function tokenise(input: string): Token[] { + const out: Token[] = []; + let i = 0; + while (i < input.length) { + const ch = input[i]!; + if (/\s/.test(ch)) { i++; continue; } + if (ch === '(') { out.push({ type: 'LPAREN', value: '(' }); i++; continue; } + if (ch === ')') { out.push({ type: 'RPAREN', value: ')' }); i++; continue; } + + // Read token until whitespace or paren + let j = i; + while (j < input.length && !/[\s()]/.test(input[j]!)) j++; + const tok = input.slice(i, j); + const upper = tok.toUpperCase(); + if (upper === 'AND') out.push({ type: 'AND', value: 'AND' }); + else if (upper === 'OR') out.push({ type: 'OR', value: 'OR' }); + else out.push({ type: 'TOKEN', value: tok }); + i = j; + } + return out; +} + +function parseTokenFilter(value: string): FilterAst { + // form: "top_by_plays:100" or "manual_ids:7280…,7281…" + const colon = value.indexOf(':'); + if (colon === -1) throw new Error(`Bad filter token: '${value}' (expected key:value)`); + const key = value.slice(0, colon); + const rhs = value.slice(colon + 1); + switch (key) { + case 'top_by_plays': return { kind: 'top_by_plays', n: parseInt(rhs, 10) }; + case 'top_by_stl': return { kind: 'top_by_stl', n: parseInt(rhs, 10) }; + case 'top_by_comments': return { kind: 'top_by_comments', n: parseInt(rhs, 10) }; + case 'top_by_saves': return { kind: 'top_by_saves', n: parseInt(rhs, 10) }; + case 'top_by_velocity': return { kind: 'top_by_velocity', n: parseInt(rhs, 10) }; + case 'manual_ids': return { kind: 'manual_ids', ids: rhs.split(/[,\s]+/).filter(Boolean) }; + default: throw new Error(`Unknown filter primitive: ${key}`); + } +} + +// Recursive-descent: AND/OR are LEFT-ASSOCIATIVE with EQUAL precedence per the V3 brief +// "left-to-right with explicit parentheses required for nesting". +function parse(tokens: Token[]): FilterAst { + let pos = 0; + function peek(): Token | undefined { return tokens[pos]; } + function consume(): Token | undefined { return tokens[pos++]; } + + function parseAtom(): FilterAst { + const t = consume(); + if (!t) throw new Error('Unexpected end of filter expression'); + if (t.type === 'LPAREN') { + const inner = parseExpr(); + const close = consume(); + if (!close || close.type !== 'RPAREN') throw new Error('Missing closing paren'); + return inner; + } + if (t.type !== 'TOKEN') throw new Error(`Unexpected token ${t.value}`); + return parseTokenFilter(t.value); + } + + function parseExpr(): FilterAst { + let left = parseAtom(); + while (peek() && (peek()!.type === 'AND' || peek()!.type === 'OR')) { + const op = consume()!; + const right = parseAtom(); + left = op.type === 'AND' ? { kind: 'AND', left, right } : { kind: 'OR', left, right }; + } + return left; + } + const ast = parseExpr(); + if (pos < tokens.length) throw new Error(`Trailing tokens after expression at position ${pos}`); + return ast; +} + +export function parseFilterExpression(raw: string): ParsedFilter { + const tokens = tokenise(raw); + if (tokens.length === 0) throw new Error('Empty filter expression'); + return { raw, expr: parse(tokens) }; +} + +export function applyFilter(videos: Pass1Video[], parsed: ParsedFilter): string[] { + const ids = evalAst(parsed.expr, videos); + return [...ids]; +} + +export function makeFilter(fn: (videos: Pass1Video[]) => string[]): FilterFn { + return (vs) => new Set(fn(vs)); +} diff --git a/v2/pipeline/lib/retry.ts b/v2/pipeline/lib/retry.ts new file mode 100644 index 0000000..4b22be1 --- /dev/null +++ b/v2/pipeline/lib/retry.ts @@ -0,0 +1,21 @@ +// 3 attempts, exponential backoff (1s, 4s, 16s) per V3 brief §4 / §13. +export async function withRetry( + fn: () => Promise, + opts: { label: string; maxAttempts?: number; backoffMs?: number[] } = { label: 'op' }, +): Promise { + const max = opts.maxAttempts ?? 3; + const backoff = opts.backoffMs ?? [1000, 4000, 16000]; + let lastErr: unknown; + for (let attempt = 1; attempt <= max; attempt++) { + try { + return await fn(); + } catch (err) { + lastErr = err; + if (attempt === max) break; + const wait = backoff[attempt - 1] ?? backoff[backoff.length - 1] ?? 1000; + console.warn(`[retry] ${opts.label} attempt ${attempt}/${max} failed: ${(err as Error).message}; retrying in ${wait}ms`); + await new Promise((r) => setTimeout(r, wait)); + } + } + throw new Error(`${opts.label} failed after ${max} attempts: ${(lastErr as Error)?.message ?? lastErr}`); +} diff --git a/v2/pipeline/lib/rubrics.ts b/v2/pipeline/lib/rubrics.ts new file mode 100644 index 0000000..4c03019 --- /dev/null +++ b/v2/pipeline/lib/rubrics.ts @@ -0,0 +1,12 @@ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROMPTS_DIR = resolve(__dirname, '..', 'prompts'); + +export function loadRubric(name: string): string { + const filename = name.endsWith('.md') ? name : `${name}.md`; + return readFileSync(resolve(PROMPTS_DIR, filename), 'utf-8'); +} diff --git a/v2/pipeline/lib/translate.ts b/v2/pipeline/lib/translate.ts new file mode 100644 index 0000000..c9c2163 --- /dev/null +++ b/v2/pipeline/lib/translate.ts @@ -0,0 +1,53 @@ +// Claude-based translation. Batches 20 comments per call to keep cost down. +import { callClaudeJSON } from './claude.js'; +import { z } from 'zod'; + +const BATCH_SCHEMA = z.object({ + translations: z.array(z.object({ i: z.number(), text_en: z.string() })), +}); + +export async function translateTextToEn(text: string, sourceLang?: string): Promise { + if (!text.trim()) return ''; + const prompt = `Translate the following text to English. If it is already English, return it verbatim.${sourceLang ? ` Source language hint: ${sourceLang}.` : ''} Preserve emoji and punctuation. Return ONLY the translated text, no commentary.\n\nTEXT:\n${text}`; + const { callClaude } = await import('./claude.js'); + const out = await callClaude(prompt, { label: 'translate_text', maxTokens: 4096 }); + return out.trim(); +} + +const BATCH_SIZE = 10; // smaller batches dodge max_tokens truncation on long comments + +async function translateBatchOnce(items: string[], sourceLang?: string): Promise { + if (items.length === 0) return []; + const numbered = items.map((t, i) => `${i + 1}. ${t.replace(/\s+/g, ' ').trim()}`).join('\n'); + const prompt = `Translate each numbered item to English. If an item is already English, copy it verbatim.${sourceLang ? ` Source language hint: ${sourceLang}.` : ''} Preserve emoji and punctuation.\n\nReturn ONLY this JSON shape (no other text):\n{"translations":[{"i":1,"text_en":"..."},{"i":2,"text_en":"..."}]}\n\nTEXTS:\n${numbered}`; + const parsed = await callClaudeJSON(prompt, { label: 'translate_batch', maxTokens: 8192 }); + const ok = BATCH_SCHEMA.safeParse(parsed); + if (!ok.success) throw new Error(`translate_batch returned bad shape: ${ok.error.message}`); + const map = new Map(ok.data.translations.map((t) => [t.i, t.text_en])); + return items.map((_orig, i) => map.get(i + 1) ?? ''); +} + +export async function translateBatchToEn(items: string[], sourceLang?: string): Promise { + if (items.length === 0) return []; + const out: string[] = []; + for (let i = 0; i < items.length; i += BATCH_SIZE) { + const slice = items.slice(i, i + BATCH_SIZE); + try { + const translated = await translateBatchOnce(slice, sourceLang); + out.push(...translated); + } catch (err) { + console.warn(`[translate] batch ${i / BATCH_SIZE + 1} failed (${(err as Error).message.slice(0, 80)}); using originals`); + out.push(...slice); + } + } + return out; +} + +/** Returns true if the string is dominantly ASCII letters/digits + common punctuation. */ +export function isLikelyEnglish(text: string): boolean { + if (!text.trim()) return true; + const ascii = text.match(/[A-Za-z]/g)?.length ?? 0; + const total = text.replace(/\s/g, '').length; + if (total === 0) return true; + return ascii / total > 0.6; +} diff --git a/v2/pipeline/prompts/atomic_insights.md b/v2/pipeline/prompts/atomic_insights.md new file mode 100644 index 0000000..d1eef35 --- /dev/null +++ b/v2/pipeline/prompts/atomic_insights.md @@ -0,0 +1,44 @@ +# Atomic insight extraction (Stage 7) + +You are extracting small, evidence-grounded observations from a batch of TikTok video analyses. The output is intermediate scaffolding — hundreds of small facts that Stage 8 will cluster into editorial trends. + +## Rules + +- Each observation must cite at least 1 video id. Most should cite 2–10. +- Err on the side of MORE, smaller insights rather than fewer, broader ones. We want 200–500 atomic insights across the whole report. +- Four types ONLY: `hook`, `visual`, `audio`, `narrative`. +- If a new observation strengthens an existing one (same pattern, more videos), ADD video ids to that atomic_id rather than creating a new one. The running list of existing observations is provided. +- Never duplicate an existing atomic_id's observation text. +- English only. + +## Type definitions + +- **hook** — a recurring opening pattern (first 3 seconds: line, gesture, framing). +- **visual** — a recurring aesthetic element (lighting, palette, composition, on-screen text style, setting, transition). +- **audio** — a recurring sound, music style, or voice device (whisper, ASMR, audio meme, voiceover cadence). +- **narrative** — a recurring thesis, tension, or worldview (what creators are saying, the conflict they keep returning to). + +## Output JSON shape + +```json +{ + "additions": [ + { + "atomic_id": "ATM-XXXX", + "type": "hook|visual|audio|narrative", + "observation": "string, ≤25 words, specific and observational", + "supporting_video_ids": ["string id", "..."] + } + ], + "extensions": [ + { + "atomic_id": "ATM-EXISTING-ID", + "added_video_ids": ["string id"] + } + ] +} +``` + +`additions` are net-new observations. `extensions` add video ids to existing atomic_ids. The orchestrator will allocate fresh ids for `additions` (the `ATM-XXXX` you supply is a within-batch placeholder; the orchestrator will rewrite to global ids). + +Return ONLY the JSON. diff --git a/v2/pipeline/prompts/category_quality.md b/v2/pipeline/prompts/category_quality.md new file mode 100644 index 0000000..6afefe3 --- /dev/null +++ b/v2/pipeline/prompts/category_quality.md @@ -0,0 +1,32 @@ +# §4.5d — Category quality rubric (Stage 8a) + +Generate 5–10 brief-driven categories. Each category groups trends in this report. + +**Good categories are:** +- Editorial and evocative (could be a magazine section name). +- Mutually exclusive (no significant overlap with another). +- 2–5 words. +- Cultural, not descriptive. + +*Good Dove examples:* "Hair Rituals", "Self-Image Drama", "Anti-Beauty Backlash", "Grooming as Identity". + +**Reject categories that are:** +- Descriptive containers ("Hair Care Videos", "Beauty Content"). +- Mechanically derived from data ("#hairtok content", "Top Plays"). +- Redundant with another ("Hair Routines" if "Hair Rituals" exists). +- Genre labels ("Tutorials", "Reviews"). + +For each rejected candidate, include `{name, reason}` in the `rejected` array so the orchestrator can re-roll. + +## Output JSON + +```json +{ + "categories": [ + {"name": "string 2-5 words", "rationale": "string"} + ], + "rejected": [{"name": "string", "reason": "string"}] +} +``` + +Return ONLY the JSON. diff --git a/v2/pipeline/prompts/editorial_naming.md b/v2/pipeline/prompts/editorial_naming.md new file mode 100644 index 0000000..cc6772e --- /dev/null +++ b/v2/pipeline/prompts/editorial_naming.md @@ -0,0 +1,17 @@ +# §4.5e — Editorial naming rubric (Stage 8b) + +Every trend needs an editorial name. The name is what a strategist will quote in the deck. + +**Good trend names are:** +- Phrased like a magazine headline or cultural call-out. +- Specific enough to be recognisable, abstract enough to hold many videos. + +*Good examples:* "The Ceremonial Hair Wash", "The 5-Minute Reset", "Anti-Influencer Beauty", "The Confession Routine". + +**Reject names that are:** +- Hashtag literals ("#hairtok trend"). +- Generic descriptors ("Hair videos", "Self-care content"). +- Feature lists ("Videos with shower scenes and ASMR"). +- Brand-supplied marketing language ("The Dove Difference"). + +If asked to QA a name, return `{ok: false, reason: "..."}` for any that fail; otherwise `{ok: true}`. diff --git a/v2/pipeline/prompts/per_video_analysis.md b/v2/pipeline/prompts/per_video_analysis.md new file mode 100644 index 0000000..b067624 --- /dev/null +++ b/v2/pipeline/prompts/per_video_analysis.md @@ -0,0 +1,64 @@ +# Per-video analysis (Stage 6) + +You are analysing a single TikTok video for a brand strategist. The video has been pre-validated: caption, transcript, comments, and frame stills are all present and tied to the same video id. Use ALL inputs. + +Your job: produce a structured JSON record describing what the video is, how it works as a piece of content, and what audience signals it carries. + +## Inputs + +You will receive: the canonical video id, the handle, plays/likes/saves/comments_count/shares/stl%, the caption + hashtags, the English transcript, up to 30 top comments (numbered, with like counts), and references to N frame stills. + +## Non-negotiable rules + +- Quote evidence verbatim. Hooks come from the transcript, audience signals come from comments. Never paraphrase a quote. +- Paid-vs-organic label uses ONLY computable signals: caption ad tags (`#ad`, `#sponsored`, `#gifted`, `#paidpartnership`), brand handle mention in caption, on-screen disclosure visible in a frame, or this creator appearing in ≥3 selected videos with brand mentions. If none fire, label is `unclear`. **Do not infer paid status from "the video looks polished".** +- English-only output. If a comment is not English, the bundle has already translated it; quote the `text_en` field. +- No marketing language. No brand voice. Editorial, observational, specific. + +## Output JSON shape (exact) + +```json +{ + "id": "string", + "what_happens": "string, 2 sentences plain description", + "hook": { + "first_3_seconds": "verbatim transcript snippet", + "pattern": "shock|question|reveal|relatable|tutorial-promise|other", + "why_it_stops_scroll": "string, 1 sentence" + }, + "visual_aesthetic": { + "lighting": "natural|harsh|soft|neon|warm|cool|mixed", + "colour_palette": ["#hex","#hex","..."], + "setting": "bathroom|bedroom|outdoor|studio|kitchen|other", + "talent": "single-creator|duo|group|none", + "products_visible": ["product names…"], + "on_screen_text_examples": ["…","…"] + }, + "format": "tutorial|confession|hot-take|review|routine|transformation|hack|skit|asmr", + "audio": { + "music_present": true, + "music_mood": "upbeat|melancholic|dreamy|aggressive|none", + "voiceover": true, + "asmr_elements": false + }, + "narrative": { + "thesis": "string, what the video is really saying", + "tension": "string, what is the conflict or interest", + "resolution": "string, how the video lands" + }, + "audience_signals": { + "comment_themes": ["theme 1","theme 2","..."], + "comment_sentiment_split": {"positive": 0, "neutral": 0, "critical": 0}, + "verbatim_quotes": [ + {"text": "verbatim english quote", "likes": 0, "theme": "label"} + ] + }, + "paid_or_organic": { + "label": "paid|organic|unclear", + "reasoning": "string, what evidence supports the label", + "evidence_signals_used": ["caption_ad_tag","caption_brand_handle","on_screen_disclosure","creator_repeat_in_report"] + } +} +``` + +Return ONLY the JSON. No prose before or after. diff --git a/v2/pipeline/prompts/relevance_calibration.md b/v2/pipeline/prompts/relevance_calibration.md new file mode 100644 index 0000000..aac4fbd --- /dev/null +++ b/v2/pipeline/prompts/relevance_calibration.md @@ -0,0 +1,27 @@ +# §4.5c — Trend relevance calibration (Stage 8b.5) + +Score each trend's `business_question_relevance` from 0.0 to 1.0. The score must be calibrated, not free-floating. Use these anchors EVERY time: + +| Score band | Tier | Definition | Worked example for "Why is hair washing emerging as a cultural moment?" | +|---|---|---|---| +| ≥0.80 | core | Trend directly answers the business question or names the territory the brand should claim. | "The Ceremonial Hair Wash": directly explains the cultural moment. **0.85** | +| 0.60–0.79 | core | Trend supports the answer materially; lead-supporting trend. | "Scalp as Self": adjacent ritual, reinforces the territory. **0.70** | +| 0.35–0.59 | peripheral | Trend gives context, useful but not the headline. | "Hair Texture Confidence": same audience, supports framing, not the answer. **0.45** | +| <0.35 | dropped | Real, well-evidenced trend that does not advance the business question. | "Skincare Minimalism": same audience, different category. **0.20** | + +Rules: +- A trend can be excellent on its own merits and still score low if it does not advance the business question. That is correct. +- Tier follows directly from score. Do not set tier independently. +- Calibrate against the anchors above; do not anchor against other trends in this report. + +## Output JSON + +```json +{ + "score": 0.0, + "tier": "core|peripheral|dropped", + "justification": "string, 1 sentence referencing the business question" +} +``` + +Return ONLY the JSON. diff --git a/v2/pipeline/prompts/seed_quality.md b/v2/pipeline/prompts/seed_quality.md new file mode 100644 index 0000000..41beb19 --- /dev/null +++ b/v2/pipeline/prompts/seed_quality.md @@ -0,0 +1,46 @@ +# Seed expansion rubric (§4.5a) + +You are seeding a TikTok social-listening scrape. Your job is to turn the brief into three tiers of hashtags, a list of search terms, and (only when high-confidence) creator handles. The rubric below is non-negotiable; every tag and term you propose must satisfy it. + +## Hashtag tiers + +- **Anchor (5–8 tags):** huge volume (millions of views in 30 days), unmistakably on-topic, native to the audience's content language. *Example for Dove on hair washing: `#hairtok`, `#showertok`, `#haircare`.* +- **Discovery (15–20 tags):** medium volume, niche-specific, where rituals and behaviours live. *Example: `#scalpcare`, `#curlyhair`, `#everythingshower`, `#hairporosity`.* +- **Edge (5–10 tags):** small but live (posts in the last 14 days), capturing emergent vocabulary. *Example: `#hairhealing`, `#scalpritual`.* + +**Reject hashtags that are:** +- Too broad (`#beauty`, `#viral`). +- Brand-locked self-references (`#dove` for the Dove brief). +- Dead (no posts in the last 14 days). +- Unrelated trends (`#mealprep` on a beauty brief). + +For each rejected candidate, include `{tag, reason}` in the `rejected` array so the user can override. + +## Search terms (10–20) + +- **Good:** how a real person describes the behaviour. *"everything shower routine", "scalp massage at night", "hair washing too much".* +- **Bad:** marketing copy ("luxurious haircare experience"), too narrow ("Dove shampoo review"), too generic ("hair tips"). + +## Creator handles + +- Only include handles you are highly confident exist (mainstream coverage, brand reports, returned in seed-research). +- Otherwise leave the array empty. Pass 1 will surface organic creators via hashtag scrapes anyway. Inventing handles wastes Apify budget. + +## Output schema + +Return ONLY valid JSON in this exact shape: + +```json +{ + "hashtags": { + "anchor": [{"tag": "#string", "rationale": "string, max 12 words"}], + "discovery": [{"tag": "#string", "rationale": "string"}], + "edge": [{"tag": "#string", "rationale": "string"}] + }, + "search_terms": [{"term": "string", "rationale": "string"}], + "handles": [{"handle": "string-no-at", "type": "brand|competitor|creator", "rationale": "string"}], + "rejected": [{"tag": "#string", "reason": "string"}] +} +``` + +All `tag` values MUST start with `#`. All `handle` values MUST NOT start with `@`. Handles array may be empty. diff --git a/v2/pipeline/prompts/trend_synthesis.md b/v2/pipeline/prompts/trend_synthesis.md new file mode 100644 index 0000000..954bb18 --- /dev/null +++ b/v2/pipeline/prompts/trend_synthesis.md @@ -0,0 +1,40 @@ +# Stage 8b — Trend synthesis + +You are clustering atomic insights into editorial trends. Target 50 trends per report; hard floor 35. **Never split a weak trend to hit a number.** + +## Inputs + +- The brief (brand, audience, business question, KPIs, context vision). +- The list of brief-driven categories already chosen for this report. +- The atomic insights list (each with id, type, observation, supporting_video_ids, frequency). +- The §4.5e editorial naming rubric (apply it to every name you choose). + +## Per-trend rules + +Each trend MUST: +- Have an editorial name conforming to §4.5e. +- Have a 2–3 sentence narrative. +- Cite at least 5 supporting `video_ids` (drawn from the atomic insights). +- Have a `category` field referencing one of the brief-driven categories exactly. +- Have a `lens_tags` array, subset of `["hooks","visual","audio","sentiment","narrative"]`. +- Have `top_atomic_ids` listing the atomic insights that anchor it. + +## Output JSON + +```json +{ + "trends": [ + { + "slug": "kebab-case-slug", + "name": "string editorial name", + "category": "string matching one of the report categories exactly", + "narrative": "2-3 sentences, observational, English", + "lens_tags": ["hooks","visual","audio","sentiment","narrative"], + "top_atomic_ids": ["ATM-XXXX","..."], + "supporting_video_ids": ["string id","..."] + } + ] +} +``` + +Return ONLY the JSON. diff --git a/v2/pipeline/stages/stage_10_build.ts b/v2/pipeline/stages/stage_10_build.ts new file mode 100644 index 0000000..e40512c --- /dev/null +++ b/v2/pipeline/stages/stage_10_build.ts @@ -0,0 +1,179 @@ +// Stage 10 — output assembly. +// +// Produces: +// outputs/dataset_v2.json — joined brief + categories + trends + lenses + qa + compare. +// outputs/dashboard.html — self-contained HTML with covers base64-inlined (≤3 MB). +// +// The full React/Vite per-report dashboard (10a) is scaffolded by Phase F-UI work +// outside this file; here we produce the data + portable claude.ai bundle. +import { writeFileSync, readFileSync, existsSync, statSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { PATHS, ensureDir } from '../lib/paths.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; +import type { Trend } from './stage_8_trends.js'; + +const COVER_INLINE_MAX_BYTES = 250_000; // ~250 KB per cover ceiling, downscaled separately + +function readJson(path: string): T | null { + if (!existsSync(path)) return null; + try { return JSON.parse(readFileSync(path, 'utf-8')) as T; } catch { return null; } +} + +function inlineCoverIfPresent(reportId: string, videoId: string): string | null { + const p = join(PATHS.enrichedVideo(reportId, videoId), 'cover.jpg'); + if (!existsSync(p)) return null; + const sz = statSync(p).size; + if (sz > COVER_INLINE_MAX_BYTES) return null; // skip oversized; stage 10a should downscale before inline + const b64 = readFileSync(p).toString('base64'); + return `data:image/jpeg;base64,${b64}`; +} + +export interface DatasetV2 { + brief: BriefInput; + generated_at: string; + categories: { name: string; rationale: string }[]; + trends: Array }>; + qa: { + paid_organic_review: unknown; + coverage_check: unknown; + }; + compare: unknown | null; + methodology: { + pass1_spend_log: unknown; + manifest_summary: unknown; + selection_rules: unknown; + }; +} + +export interface Stage10Result { + ok: true; + outputs: Record; + dataset_size_bytes: number; + html_size_bytes: number; + trend_count: number; + inlined_covers: number; +} + +export async function runStage10Build(reportId: string, brief: BriefInput): Promise { + ensureDir(PATHS.outputsDir(reportId)); + + const trends = readJson(PATHS.trends(reportId)) ?? []; + const categoriesData = readJson<{ categories: { name: string; rationale: string }[] }>(PATHS.categories(reportId)); + const paidOrganic = readJson(join(PATHS.qaDir(reportId), 'paid_organic_review.json')); + const coverage = readJson(join(PATHS.qaDir(reportId), 'coverage_check.json')); + const spendLog = readJson(PATHS.spendLog(reportId)); + const selectionRules = readJson(PATHS.selectionRules(reportId)); + const manifestData = readJson<{ summary: unknown }>(PATHS.manifestJson(reportId)); + + // Lift handle/plays/stl from pass1 to enrich top_videos. + const pass1 = readJson>(PATHS.pass1Videos(reportId)) ?? []; + const pass1Map = new Map(pass1.map((v) => [v.id, v])); + + // Compose the dataset. + let inlined = 0; + const enrichedTrends = trends.map((t) => { + const supporting = t.supporting_video_ids.slice(0, 8); + const top_videos = supporting.map((id) => { + const p = pass1Map.get(id); + const cover_b64 = inlineCoverIfPresent(reportId, id); + if (cover_b64) inlined++; + return { + id, + handle: p?.handle ?? 'unknown', + plays: p?.plays ?? 0, + stl_pct: p?.stl_pct ?? 0, + cover_b64, + }; + }); + return { ...t, top_videos }; + }); + + const compare = readJson(join(PATHS.outputsDir(reportId), 'compare', 'returning_trends.json')); + + const dataset: DatasetV2 = { + brief, + generated_at: new Date().toISOString(), + categories: categoriesData?.categories ?? [], + trends: enrichedTrends, + qa: { paid_organic_review: paidOrganic, coverage_check: coverage }, + compare: compare ? { + new_trends: readJson(join(PATHS.outputsDir(reportId), 'compare', 'new_trends.json')), + returning_trends: compare, + faded_trends: readJson(join(PATHS.outputsDir(reportId), 'compare', 'faded_trends.json')), + category_momentum:readJson(join(PATHS.outputsDir(reportId), 'compare', 'category_momentum.json')), + } : null, + methodology: { + pass1_spend_log: spendLog, + manifest_summary: manifestData?.summary ?? null, + selection_rules: selectionRules, + }, + }; + + const datasetJson = JSON.stringify(dataset, null, 2); + const datasetPath = PATHS.datasetV2(reportId); + writeFileSync(datasetPath, datasetJson); + + // Self-contained HTML bundle (10b). Minimal skeleton — claude.ai will render rich UI on upload. + const html = ` + + + + +${escapeHtml(brief.client_name)} — Social Listening V2 + + + +
+

${escapeHtml(brief.client_name)} — Social Listening

+
${escapeHtml(brief.business_question)}
+
${dataset.trends.length} trends across ${dataset.categories.length} categories • generated ${new Date(dataset.generated_at).toUTCString()}
+
+
+${dataset.trends.map((t) => ` +
+ ${t.business_question_relevance.tier} + ${escapeHtml(t.category)} +

${escapeHtml(t.name)}

+

${escapeHtml(t.narrative)}

+
`).join('\n')} +
+ + +`; + + const htmlPath = PATHS.dashboardHtml(reportId); + writeFileSync(htmlPath, html); + + return { + ok: true, + outputs: { dataset_v2: datasetPath, dashboard_html: htmlPath }, + dataset_size_bytes: Buffer.byteLength(datasetJson), + html_size_bytes: Buffer.byteLength(html), + trend_count: trends.length, + inlined_covers: inlined, + }; +} + +function escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function escapeJsonForScript(s: string): string { + // Avoid ending the script tag inside the JSON. + return s.replace(/<\/script>/gi, '<\\/script>'); +} + +mkdirSync; // touch (used inside ensureDir, kept here for ESM clarity) diff --git a/v2/pipeline/stages/stage_1_seeds.ts b/v2/pipeline/stages/stage_1_seeds.ts new file mode 100644 index 0000000..b992130 --- /dev/null +++ b/v2/pipeline/stages/stage_1_seeds.ts @@ -0,0 +1,72 @@ +// Stage 1: turn a brief into a tier-labelled seed list using the §4.5a rubric. +import { writeFileSync } from 'node:fs'; +import { z } from 'zod'; +import { callClaudeJSON } from '../lib/claude.js'; +import { loadRubric } from '../lib/rubrics.js'; +import { PATHS } from '../lib/paths.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; + +export const SEEDS_SCHEMA = z.object({ + hashtags: z.object({ + anchor: z.array(z.object({ tag: z.string().startsWith('#'), rationale: z.string() })), + discovery: z.array(z.object({ tag: z.string().startsWith('#'), rationale: z.string() })), + edge: z.array(z.object({ tag: z.string().startsWith('#'), rationale: z.string() })), + }), + search_terms: z.array(z.object({ term: z.string().min(1), rationale: z.string() })), + handles: z.array(z.object({ + handle: z.string().min(1).regex(/^[^@]/, 'handles must not start with @'), + type: z.enum(['brand', 'competitor', 'creator']), + rationale: z.string(), + })), + rejected: z.array(z.object({ tag: z.string(), reason: z.string() })).default([]), +}); + +export type Seeds = z.infer; + +export interface StageRunResult { + ok: boolean; + outputs: Record; +} + +export async function runStage1Seeds(input: { reportId: string; brief: BriefInput }): Promise { + const rubric = loadRubric('seed_quality'); + const briefBlock = `# Brief\n\n` + + `Brand: ${input.brief.brand.name} (@${input.brief.brand.handle})\n` + + (input.brief.brand.positioning ? `Positioning: ${input.brief.brand.positioning}\n` : '') + + `Category: ${input.brief.category}\n` + + `Geo: ${input.brief.geo} | Language: ${input.brief.language}\n` + + `Audience: ${input.brief.audience.primary} (${input.brief.audience.age_range} ${input.brief.audience.gender})\n` + + `Audience interests: ${input.brief.audience.interests.join(', ')}\n` + + `Competitors: ${input.brief.competitors.map((c) => `${c.name} (@${c.handle})`).join(', ')}\n` + + `Business question: ${input.brief.business_question}\n` + + `KPIs: ${input.brief.kpis.join(' | ')}\n` + + (input.brief.context_vision ? `\nReport context / vision:\n${input.brief.context_vision}\n` : ''); + + const prompt = `${rubric}\n\n---\n\n${briefBlock}\n\nReturn the seeds JSON now.`; + + const raw = await callClaudeJSON(prompt, { label: 'stage_1_seeds', maxTokens: 8192 }); + const parsed = SEEDS_SCHEMA.safeParse(raw); + if (!parsed.success) { + throw new Error(`Stage 1 returned invalid seeds JSON: ${parsed.error.message}`); + } + + // Inject brand + competitor handles unconditionally if Claude didn't list them. + const handleSet = new Set(parsed.data.handles.map((h) => h.handle.toLowerCase())); + const enriched = { ...parsed.data, handles: [...parsed.data.handles] }; + if (!handleSet.has(input.brief.brand.handle.toLowerCase())) { + enriched.handles.unshift({ + handle: input.brief.brand.handle, type: 'brand', rationale: 'Brand from brief', + }); + } + for (const c of input.brief.competitors) { + if (!handleSet.has(c.handle.toLowerCase())) { + enriched.handles.push({ + handle: c.handle, type: 'competitor', rationale: `Competitor: ${c.name}`, + }); + } + } + + const outPath = PATHS.seedsJson(input.reportId); + writeFileSync(outPath, JSON.stringify(enriched, null, 2)); + return { ok: true, outputs: { seeds: outPath } }; +} diff --git a/v2/pipeline/stages/stage_2_pass1_scrape.ts b/v2/pipeline/stages/stage_2_pass1_scrape.ts new file mode 100644 index 0000000..f861d5c --- /dev/null +++ b/v2/pipeline/stages/stage_2_pass1_scrape.ts @@ -0,0 +1,275 @@ +// Stage 2: broad TikTok pull driven by seeds.json. Budget-bounded; date-filtered; +// engagement-floored; deduped by canonical TikTok video id. Writes pass1_videos.json +// and a spend log that tracks raw_returned vs kept_after_floor per scrape. +import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'; +import { ACTORS, defaultLimits, runActor, resetBudget, setSoftCap, getRunningCost, isBudgetExceeded, onApifyCost } from '../lib/apify_client.js'; +import { extractTikTokId, canonicalTikTokUrl } from '../lib/ids.js'; +import { applyEngagementFloor, type EngagementFloor, type EngagementCounters, computeStlPct } from '../lib/engagement_floor.js'; +import { PATHS } from '../lib/paths.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; +import type { Seeds } from './stage_1_seeds.js'; + +export interface Pass1Video { + id: string; + handle: string; + url_canonical: string; + caption: string; + hashtags: string[]; + plays: number; + likes: number; + saves: number; + comments_count: number; + shares: number; + stl_pct: number; + duration_sec: number; + posted_at: string; // ISO + cover: string | null; + /** mp4 direct download URL — ephemeral (~14 day TTL). Stage 4 fetches within hours. */ + download_url: string | null; + _source: string; // e.g. "hashtag:hairtok", "profile:dove", "search:everything shower" + _scraped_at: string; +} + +interface RawTikTok { + id?: string; + webVideoUrl?: string; + videoUrl?: string; + url?: string; + authorMeta?: { name?: string }; + text?: string; + hashtags?: Array<{ name?: string } | string>; + playCount?: number; + diggCount?: number; + collectCount?: number; + commentCount?: number; + shareCount?: number; + videoMeta?: { + duration?: number; + downloadAddr?: string; + coverUrl?: string; + originalCoverUrl?: string; + }; + mediaUrls?: string[]; + createTimeISO?: string; + createTime?: number; + covers?: { default?: string }; +} + +function parseDate(raw: RawTikTok): string | null { + if (raw.createTimeISO) return new Date(raw.createTimeISO).toISOString(); + if (typeof raw.createTime === 'number') return new Date(raw.createTime * 1000).toISOString(); + return null; +} + +function normaliseTags(raw: RawTikTok['hashtags']): string[] { + if (!Array.isArray(raw)) return []; + return raw.map((h) => (typeof h === 'string' ? h : h?.name)).filter((t): t is string => !!t).map((t) => t.startsWith('#') ? t.toLowerCase() : `#${t.toLowerCase()}`); +} + +function normaliseRaw(raw: RawTikTok, source: string): Pass1Video | null { + const url = raw.webVideoUrl || raw.videoUrl || raw.url || ''; + const id = extractTikTokId(raw.id || url); + if (!id) return null; + const handle = (raw.authorMeta?.name || '').replace(/^@/, ''); + if (!handle) return null; + const posted = parseDate(raw); + if (!posted) return null; + const plays = raw.playCount ?? 0; + const likes = raw.diggCount ?? 0; + const saves = raw.collectCount ?? 0; + const comments = raw.commentCount ?? 0; + const shares = raw.shareCount ?? 0; + const stl = computeStlPct({ plays, likes, saves, comments_count: comments, shares }); + return { + id, handle, + url_canonical: canonicalTikTokUrl(id, handle), + caption: raw.text || '', + hashtags: normaliseTags(raw.hashtags), + plays, likes, saves, + comments_count: comments, + shares, + stl_pct: Math.round(stl * 100) / 100, + duration_sec: raw.videoMeta?.duration ?? 0, + posted_at: posted, + // hashtag scraper returns cover URL at videoMeta.coverUrl/originalCoverUrl; + // older actor versions used `covers.default`. Try every shape. + cover: raw.videoMeta?.coverUrl ?? raw.videoMeta?.originalCoverUrl ?? raw.covers?.default ?? null, + // mp4 download is empty unless `shouldDownloadVideos: true` was passed; we keep + // the URL if present, otherwise accept that frame extraction is best-effort. + download_url: raw.videoMeta?.downloadAddr ?? raw.mediaUrls?.[0] ?? null, + _source: source, + _scraped_at: new Date().toISOString(), + }; +} + +interface SpendEntry { + label: string; + source_kind: 'hashtag' | 'profile' | 'search'; + source_value: string; + cost_usd: number; + run_id: string; + raw_returned: number; + kept_after_floor: number; + kept_after_dedup: number; + floor_counters: EngagementCounters; +} + +interface Stage2Args { + reportId: string; + brief: BriefInput; +} + +function inDateWindow(iso: string, days: number): boolean { + const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; + return new Date(iso).getTime() >= cutoff; +} + +export async function runStage2Pass1Scrape(args: Stage2Args): Promise<{ ok: true; outputs: Record; total_videos: number; total_cost_usd: number }> { + const { reportId, brief } = args; + const seedsPath = PATHS.seedsJson(reportId); + if (!existsSync(seedsPath)) throw new Error(`seeds.json missing at ${seedsPath}. Run stage 1 first.`); + const seeds = JSON.parse(readFileSync(seedsPath, 'utf-8')) as Seeds; + + // Budget: hard ceiling 95% of brief.budget_usd, soft Pass-1 cap 50%. + const hardCeiling = brief.budget_usd * 0.95; + const pass1Cap = brief.budget_usd * 0.5; + resetBudget({ hardCeilingUsd: hardCeiling }); + setSoftCap(pass1Cap); + + mkdirSync(PATHS.pass1(reportId), { recursive: true }); + const rawDumpsDir = `${PATHS.pass1(reportId)}/raw`; + mkdirSync(rawDumpsDir, { recursive: true }); + + const seenIds = new Map(); + const spendLog: SpendEntry[] = []; + const floor: EngagementFloor = { + min_likes: brief.min_likes, + min_plays: brief.min_plays, + min_stl_pct: brief.min_stl_pct, + }; + const limits = defaultLimits(); + const dateDays = brief.date_window_days; + + // Cost callback writes raw cost to a side log; per-scrape totals computed below. + onApifyCost(() => { /* aggregated through getRunningCost() */ }); + + type ScrapeJob = + | { kind: 'hashtag'; tag: string; tier: 'anchor' | 'discovery' | 'edge' } + | { kind: 'profile'; handle: string } + | { kind: 'search'; term: string }; + + const order: ScrapeJob[] = []; + for (const t of seeds.hashtags.anchor) order.push({ kind: 'hashtag', tag: t.tag, tier: 'anchor' }); + for (const h of seeds.handles) order.push({ kind: 'profile', handle: h.handle }); + for (const t of seeds.hashtags.discovery) order.push({ kind: 'hashtag', tag: t.tag, tier: 'discovery' }); + for (const s of seeds.search_terms) order.push({ kind: 'search', term: s.term }); + for (const t of seeds.hashtags.edge) order.push({ kind: 'hashtag', tag: t.tag, tier: 'edge' }); + + for (const job of order) { + if (isBudgetExceeded()) { + console.log(`[stage 2] Pass-1 cap reached at $${getRunningCost().toFixed(2)} — stopping`); + break; + } + const label = job.kind === 'hashtag' + ? `hashtag:${job.tag} (${job.tier})` + : job.kind === 'profile' + ? `profile:${job.handle}` + : `search:${job.term}`; + + let actor: string; + let input: Record; + if (job.kind === 'hashtag') { + actor = ACTORS.TIKTOK_HASHTAG; + input = { + hashtags: [job.tag.replace(/^#/, '')], + resultsPerPage: limits.resultsPerPage, + shouldDownloadVideos: false, + shouldDownloadCovers: false, + proxyCountryCode: brief.geo, + // engagement floor applied actor-side where supported + minPlayCount: brief.min_plays, + }; + } else if (job.kind === 'profile') { + actor = ACTORS.TIKTOK_PROFILE; + input = { + profiles: [job.handle.replace(/^@/, '')], + resultsPerPage: limits.resultsPerPage, + shouldDownloadVideos: false, + shouldDownloadCovers: false, + }; + } else { + actor = ACTORS.TIKTOK_HASHTAG; // hashtag actor accepts search terms via "searchQueries" + input = { + searchQueries: [job.term], + resultsPerPage: limits.resultsPerPage, + shouldDownloadVideos: false, + shouldDownloadCovers: false, + proxyCountryCode: brief.geo, + minPlayCount: brief.min_plays, + }; + } + + let res; + try { + res = await runActor(actor, input, label); + } catch (err) { + console.warn(`[stage 2] ${label} failed: ${(err as Error).message}`); + continue; + } + + // Persist raw dump for forensics + if (res.status === 'OK') { + writeFileSync(`${rawDumpsDir}/${res.run_id}.json`, JSON.stringify(res.items, null, 2)); + } + + const sourceTag = job.kind === 'hashtag' ? `hashtag:${job.tag}` : job.kind === 'profile' ? `profile:${job.handle}` : `search:${job.term}`; + const normalised: Pass1Video[] = []; + for (const raw of res.items) { + const v = normaliseRaw(raw, sourceTag); + if (!v) continue; + if (!inDateWindow(v.posted_at, dateDays)) continue; + normalised.push(v); + } + + const { kept, counters } = applyEngagementFloor(normalised, floor); + + let newCount = 0; + for (const v of kept) { + if (!seenIds.has(v.id)) { seenIds.set(v.id, v); newCount++; } + } + + spendLog.push({ + label, + source_kind: job.kind, + source_value: job.kind === 'hashtag' ? job.tag : job.kind === 'profile' ? job.handle : job.term, + cost_usd: res.cost_usd, + run_id: res.run_id, + raw_returned: res.items.length, + kept_after_floor: counters.kept_after_floor, + kept_after_dedup: newCount, + floor_counters: counters, + }); + console.log(`[stage 2] ${label}: raw=${res.items.length} → kept=${counters.kept_after_floor} → new=${newCount}`); + } + + const allVideos = [...seenIds.values()].sort((a, b) => b.plays - a.plays); + writeFileSync(PATHS.pass1Videos(reportId), JSON.stringify(allVideos, null, 2)); + writeFileSync(PATHS.spendLog(reportId), JSON.stringify({ + pass: 'pass1', + hard_ceiling_usd: hardCeiling, + pass1_cap_usd: pass1Cap, + total_cost_usd: getRunningCost(), + total_videos: allVideos.length, + entries: spendLog, + }, null, 2)); + + return { + ok: true, + outputs: { + pass1_videos: PATHS.pass1Videos(reportId), + spend_log: PATHS.spendLog(reportId), + }, + total_videos: allVideos.length, + total_cost_usd: getRunningCost(), + }; +} diff --git a/v2/pipeline/stages/stage_3_select.ts b/v2/pipeline/stages/stage_3_select.ts new file mode 100644 index 0000000..4a6b653 --- /dev/null +++ b/v2/pipeline/stages/stage_3_select.ts @@ -0,0 +1,88 @@ +// Stage 3: pick which videos go deep. Recipe-led; user can accept or override. +// Output: selected_video_ids.json + selection_rules.json (audit trail). +import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'; +import { matchRecipe, parseFilterExpression, applyFilter, RECIPES, type RecipeId } from '../lib/recipes.js'; +import { PATHS } from '../lib/paths.js'; +import type { Pass1Video } from './stage_2_pass1_scrape.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; + +export interface SelectionRules { + recipe_id: RecipeId; + recipe_name: string; + filter_expression: string; + filter_source: 'recipe_default' | 'user_override' | 'custom'; + total_pass1: number; + selected_count: number; + business_question: string; + applied_at: string; +} + +export interface Stage3Args { + reportId: string; + brief: BriefInput; + /** force a specific recipe (CLI: --recipe A|B|C|D). */ + forceRecipe?: RecipeId; + /** user-supplied custom filter expression (CLI: --custom "..."). */ + customFilter?: string; +} + +export async function runStage3Select(args: Stage3Args): Promise<{ ok: true; outputs: Record; selected: string[]; rules: SelectionRules }> { + const { reportId, brief, forceRecipe, customFilter } = args; + const pass1Path = PATHS.pass1Videos(reportId); + if (!existsSync(pass1Path)) throw new Error(`pass1_videos.json missing at ${pass1Path}. Run stage 2 first.`); + + const videos = JSON.parse(readFileSync(pass1Path, 'utf-8')) as Pass1Video[]; + if (videos.length === 0) throw new Error('No pass1 videos to select from.'); + + let recipeId: RecipeId; + let filterExpression: string; + let filterSource: SelectionRules['filter_source']; + + if (customFilter) { + recipeId = forceRecipe ?? matchRecipe(brief.business_question); + filterExpression = customFilter; + filterSource = 'custom'; + } else if (forceRecipe) { + recipeId = forceRecipe; + filterExpression = RECIPES[forceRecipe].default_filter; + filterSource = 'user_override'; + } else { + recipeId = matchRecipe(brief.business_question); + filterExpression = RECIPES[recipeId].default_filter; + filterSource = 'recipe_default'; + } + + const parsed = parseFilterExpression(filterExpression); + const selected = applyFilter(videos, parsed); + + // Sort selected ids by their pass1 plays rank for stable downstream behaviour. + const playsRank = new Map(videos.map((v, i) => [v.id, i])); + selected.sort((a, b) => (playsRank.get(a) ?? Number.MAX_SAFE_INTEGER) - (playsRank.get(b) ?? Number.MAX_SAFE_INTEGER)); + + const rules: SelectionRules = { + recipe_id: recipeId, + recipe_name: RECIPES[recipeId].name, + filter_expression: filterExpression, + filter_source: filterSource, + total_pass1: videos.length, + selected_count: selected.length, + business_question: brief.business_question, + applied_at: new Date().toISOString(), + }; + + mkdirSync(PATHS.pass2(reportId), { recursive: true }); + writeFileSync(PATHS.selectedIds(reportId), JSON.stringify(selected, null, 2)); + writeFileSync(PATHS.selectionRules(reportId), JSON.stringify(rules, null, 2)); + + console.log(`[stage 3] recipe ${recipeId} (${RECIPES[recipeId].name}) → ${selected.length} videos selected from ${videos.length}`); + + return { + ok: true, + outputs: { + selected_ids: PATHS.selectedIds(reportId), + selection_rules: PATHS.selectionRules(reportId), + }, + selected, + rules, + }; +} diff --git a/v2/pipeline/stages/stage_4_pass2_enrich.ts b/v2/pipeline/stages/stage_4_pass2_enrich.ts new file mode 100644 index 0000000..cd43e14 --- /dev/null +++ b/v2/pipeline/stages/stage_4_pass2_enrich.ts @@ -0,0 +1,393 @@ +// Stage 4 — deep per-video enrichment. THE LINKING FIX. +// +// Every Apify response is matched back to the canonical TikTok id via +// extractTikTokId, never via URL string equality. Drift is logged loudly to +// drift_log.jsonl and surfaces as `failed` assets in the manifest. +// +// Per-video folder layout (V3 brief §4 stage 4): +// enriched/{video_id}/ +// metadata.json +// cover.jpg +// mp4.bin (downloaded for frame extraction; deleted after) +// transcript.json { language_detected, text_original, text_en, source } +// comments.json [{rank, author_handle, text_original, text_en, likes, replies_count, posted_at}] +// frames/0001.jpg, 0002.jpg, … +// bundle.json — last write, the only file Stage 6 reads +import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { ACTORS, runActor } from '../lib/apify_client.js'; +import { extractTikTokId } from '../lib/ids.js'; +import { logDrift, resetDriftCounter, clearDriftLog, getDriftCount } from '../lib/drift_log.js'; +import { withRetry } from '../lib/retry.js'; +import { extractFrames } from '../lib/frames.js'; +import { translateTextToEn, translateBatchToEn, isLikelyEnglish } from '../lib/translate.js'; +import { PATHS } from '../lib/paths.js'; +import type { Pass1Video } from './stage_2_pass1_scrape.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; + +interface RawTranscript { + videoUrl?: string; + postUrl?: string; + url?: string; + webVideoUrl?: string; + videoWebUrl?: string; + submittedVideoUrl?: string; + input?: string; + id?: string; + language?: string; + text?: string; + subtitles?: string; + transcript?: string; // emQXBCL3xePZYgJyn returns WEBVTT in this field + success?: boolean; +} + +interface RawComment { + videoUrl?: string; + postUrl?: string; + url?: string; + webVideoUrl?: string; + videoWebUrl?: string; // BDec00yAmCm1QbMEI uses this + submittedVideoUrl?: string; // and this + input?: string; // and this (echoes the input URL) + text?: string; + uniqueId?: string; + user?: { uniqueId?: string }; + diggCount?: number; + likeCount?: number; + replyCount?: number; + replyCommentTotal?: number; + createTime?: number; + createTimeISO?: string; +} + +interface BundleTranscript { language_detected: string; text_original: string; text_en: string; source: 'apify-tiktok-subtitles' } + +interface BundleComment { + rank: number; + author_handle: string; + text_original: string; + text_en: string; + likes: number; + replies_count: number; + posted_at: string; +} + +export interface VideoBundle { + id: string; + url: string; + handle: string; + metadata: Pass1Video; + transcript: BundleTranscript | null; + comments: BundleComment[]; + frames: Array<{ index: number; path: string }>; + cover_local: string | null; + _validation: { all_ok: boolean; checked_at: string; missing: string[] }; +} + +export interface DroppedVideo { + id: string; + reason: string; + handle?: string; +} + +const TARGET_COMMENTS = 30; +const MIN_COMMENTS = 5; +const MAX_CONCURRENCY = 4; + +// Group raw items by canonical id; record drift on the way. +// Exported so unit tests can verify the V1-bug fix (URL drift → null asset) without +// running a real Apify call. +// +// Drifty actors return URL field with non-deterministic names: +// - TIKTOK_HASHTAG/PROFILE: `webVideoUrl` +// - TIKTOK_TRANSCRIPTS: `url` + `id` (numeric) +// - TIKTOK_COMMENTS: `videoWebUrl`, `submittedVideoUrl`, `input` +// We try every field; if a numeric `id` is present we use that directly. +export function groupByCanonicalId( + reportId: string, + actorLabel: string, + items: T[], + selectedIds: Set, +): Map { + const out = new Map(); + for (const item of items) { + const sourceUrl = + item.webVideoUrl || item.videoWebUrl || item.submittedVideoUrl || + item.videoUrl || item.postUrl || item.url || item.input || ''; + const id = extractTikTokId(item.id ?? sourceUrl); + if (!id) { + logDrift(reportId, { actor: actorLabel, reason: 'no-id-extracted', source_url: sourceUrl || null, extracted_id: null, context: { item_keys: Object.keys(item) } }); + continue; + } + if (!selectedIds.has(id)) { + logDrift(reportId, { actor: actorLabel, reason: 'id-not-in-selection', source_url: sourceUrl || null, extracted_id: id }); + continue; + } + const arr = out.get(id) ?? []; + arr.push(item); + out.set(id, arr); + } + return out; +} + +async function downloadFile(url: string, dest: string): Promise<{ ok: boolean; bytes: number; error?: string }> { + try { + const res = await fetch(url); + if (!res.ok) return { ok: false, bytes: 0, error: `HTTP ${res.status}` }; + const buf = Buffer.from(await res.arrayBuffer()); + writeFileSync(dest, buf); + return { ok: true, bytes: buf.length }; + } catch (err) { + return { ok: false, bytes: 0, error: (err as Error).message }; + } +} + +async function processOneVideo(opts: { + reportId: string; + meta: Pass1Video; + transcripts: Map; + commentsByVid: Map; +}): Promise<{ id: string; bundle?: VideoBundle; dropped?: DroppedVideo }> { + const { reportId, meta, transcripts, commentsByVid } = opts; + const id = meta.id; + const dir = PATHS.enrichedVideo(reportId, id); + mkdirSync(dir, { recursive: true }); + + // 1. metadata.json — always written (re-derivable from pass1, but we want self-containment) + writeFileSync(join(dir, 'metadata.json'), JSON.stringify(meta, null, 2)); + + // 2. cover.jpg + let coverLocal: string | null = null; + if (meta.cover) { + const dest = join(dir, 'cover.jpg'); + const dl = await withRetry(() => downloadFile(meta.cover!, dest), { label: `cover ${id}` }) + .catch((e) => ({ ok: false, bytes: 0, error: (e as Error).message })); + if (dl.ok && dl.bytes > 5_000) coverLocal = dest; + } + + // 3. transcript.json — must have non-empty text or video is dropped + const tArr = transcripts.get(id) ?? []; + const tRaw = tArr[0]; + const rawTranscript = tRaw?.transcript || tRaw?.text || tRaw?.subtitles || ''; + // Strip WEBVTT formatting (timestamps + cue blocks) — keep spoken text only. + const transcriptText = rawTranscript + .replace(/^WEBVTT[\r\n]+/i, '') + .replace(/\d{2}:\d{2}:\d{2}\.\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}\.\d{3}.*$/gm, '') + .replace(/^\d+\s*$/gm, '') // cue numbers + .replace(/\n{3,}/g, '\n\n') + .trim(); + let transcriptBundle: BundleTranscript | null = null; + if (transcriptText) { + const lang = (tRaw?.language || '').toLowerCase(); + const looksEn = lang === 'en' || lang.startsWith('en-') || (lang === '' && isLikelyEnglish(transcriptText)); + let text_en = transcriptText; + if (!looksEn) { + try { + text_en = await withRetry(() => translateTextToEn(transcriptText, lang || undefined), { label: `translate transcript ${id}` }); + } catch (err) { + text_en = transcriptText; // best-effort: keep original if translation fails + console.warn(`[stage 4] transcript translation failed for ${id}: ${(err as Error).message}`); + } + } + transcriptBundle = { language_detected: lang || 'unknown', text_original: transcriptText, text_en, source: 'apify-tiktok-subtitles' }; + writeFileSync(join(dir, 'transcript.json'), JSON.stringify(transcriptBundle, null, 2)); + } + + // 4. comments.json — target 30, minimum 5; below 5 video is dropped from selection + const rawComments = (commentsByVid.get(id) ?? []).slice().sort((a, b) => (b.diggCount ?? b.likeCount ?? 0) - (a.diggCount ?? a.likeCount ?? 0)); + const top = rawComments.slice(0, TARGET_COMMENTS); + const commentBundle: BundleComment[] = []; + if (top.length >= MIN_COMMENTS) { + const langGuess = transcriptBundle?.language_detected || 'unknown'; + const looksEn = langGuess === 'en' || langGuess.startsWith('en-'); + const originals = top.map((c) => (c.text || '').trim()); + let translations: string[]; + if (looksEn || originals.every(isLikelyEnglish)) { + translations = originals; + } else { + try { + translations = await withRetry(() => translateBatchToEn(originals, looksEn ? undefined : langGuess), { label: `translate comments ${id}` }); + } catch (err) { + console.warn(`[stage 4] comments translation failed for ${id}: ${(err as Error).message}`); + translations = originals; + } + } + for (let i = 0; i < top.length; i++) { + const c = top[i]!; + const posted = c.createTimeISO ?? (typeof c.createTime === 'number' ? new Date(c.createTime * 1000).toISOString() : ''); + commentBundle.push({ + rank: i + 1, + author_handle: c.uniqueId || c.user?.uniqueId || 'unknown', + text_original: originals[i] ?? '', + text_en: translations[i] ?? originals[i] ?? '', + likes: c.diggCount ?? c.likeCount ?? 0, + replies_count: c.replyCommentTotal ?? c.replyCount ?? 0, + posted_at: posted, + }); + } + writeFileSync(join(dir, 'comments.json'), JSON.stringify(commentBundle, null, 2)); + } + + // 5. frames — best-effort. mp4 URL is ephemeral; if it expired, log and continue. + let frames: Array<{ index: number; path: string }> = []; + if (meta.download_url) { + const mp4Path = join(dir, 'mp4.bin'); + const dl = await withRetry(() => downloadFile(meta.download_url!, mp4Path), { label: `mp4 ${id}` }) + .catch((e) => ({ ok: false, bytes: 0, error: (e as Error).message })); + if (dl.ok && dl.bytes > 50_000) { + const result = extractFrames({ mp4Path, outDir: join(dir, 'frames'), durationSec: meta.duration_sec }); + if (result.ok) { + frames = result.frames.map((name, i) => ({ index: i + 1, path: `frames/${name}` })); + } else { + logDrift(reportId, { actor: 'ffmpeg', reason: 'metadata-missing-fields', source_url: meta.download_url, extracted_id: id, context: { error: result.error } }); + } + try { rmSync(mp4Path); } catch { /* non-fatal */ } + } else { + logDrift(reportId, { actor: 'mp4-download', reason: 'metadata-missing-fields', source_url: meta.download_url, extracted_id: id, context: { error: dl.error ?? 'download failed', bytes: dl.bytes } }); + } + } + + // 6. Validate the per-video bundle and write it last (Stage 6 reads only this file). + const missing: string[] = []; + if (!transcriptBundle) missing.push('transcript'); + if (commentBundle.length < MIN_COMMENTS) missing.push('comments'); + if (frames.length < 3) missing.push('frames'); + if (!coverLocal) missing.push('cover'); + + // If transcript or comments are below threshold, mark as dropped — bundle still written for forensics. + const dropped = missing.includes('transcript') || missing.includes('comments') + ? { id, handle: meta.handle, reason: `Missing required asset(s): ${missing.join(', ')}` } + : undefined; + + const bundle: VideoBundle = { + id, + url: meta.url_canonical, + handle: meta.handle, + metadata: meta, + transcript: transcriptBundle, + comments: commentBundle, + frames, + cover_local: coverLocal, + _validation: { all_ok: missing.length === 0, checked_at: new Date().toISOString(), missing }, + }; + writeFileSync(join(dir, 'bundle.json'), JSON.stringify(bundle, null, 2)); + + return dropped ? { id, dropped } : { id, bundle }; +} + +async function inFlight(items: T[], concurrency: number, fn: (x: T) => Promise): Promise { + const results: R[] = []; + let i = 0; + async function worker(): Promise { + while (i < items.length) { + const idx = i++; + const item = items[idx]!; + results[idx] = await fn(item); + } + } + const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker()); + await Promise.all(workers); + return results; +} + +export interface Stage4Args { + reportId: string; + brief: BriefInput; + /** override for tests / partial reruns. */ + onlyIds?: string[]; +} + +export interface Stage4Result { + ok: true; + outputs: Record; + total_attempted: number; + total_bundled: number; + total_dropped: number; + drift_events: number; +} + +export async function runStage4Pass2Enrich(args: Stage4Args): Promise { + const { reportId, onlyIds } = args; + resetDriftCounter(); + clearDriftLog(reportId); + + const selectedPath = PATHS.selectedIds(reportId); + const pass1Path = PATHS.pass1Videos(reportId); + if (!existsSync(selectedPath)) throw new Error(`selected_video_ids.json missing. Run select first.`); + if (!existsSync(pass1Path)) throw new Error(`pass1_videos.json missing. Run scrape1 first.`); + + const allSelected: string[] = JSON.parse(readFileSync(selectedPath, 'utf-8')); + const ids = onlyIds ?? allSelected; + const idSet = new Set(ids); + const pass1: Pass1Video[] = JSON.parse(readFileSync(pass1Path, 'utf-8')); + const metaById = new Map(pass1.map((v) => [v.id, v])); + + // Bulk Apify calls — one per actor for the entire selection set. Cheaper than per-video. + // We CACHE the raw responses to disk so reruns can skip Apify entirely. + const urls = ids.map((id) => metaById.get(id)?.url_canonical).filter((u): u is string => !!u); + const tCachePath = `${PATHS.pass2(reportId)}/_cache_transcripts.json`; + const cCachePath = `${PATHS.pass2(reportId)}/_cache_comments.json`; + + let tItems: RawTranscript[] = []; + if (existsSync(tCachePath)) { + console.log('[stage 4] using cached transcripts response (no Apify call)'); + tItems = JSON.parse(readFileSync(tCachePath, 'utf-8')); + } else { + console.log(`[stage 4] bulk transcripts call for ${urls.length} videos`); + const tRes = await runActor(ACTORS.TIKTOK_TRANSCRIPTS, { videos: urls }, 'TIKTOK_TRANSCRIPTS'); + tItems = tRes.items; + writeFileSync(tCachePath, JSON.stringify(tItems)); + } + const transcripts = groupByCanonicalId(reportId, 'TIKTOK_TRANSCRIPTS', tItems, idSet); + + let cItems: RawComment[] = []; + if (existsSync(cCachePath)) { + console.log('[stage 4] using cached comments response (no Apify call)'); + cItems = JSON.parse(readFileSync(cCachePath, 'utf-8')); + } else { + console.log(`[stage 4] bulk comments call for ${urls.length} videos`); + const cRes = await runActor(ACTORS.TIKTOK_COMMENTS, { postURLs: urls, maxComments: TARGET_COMMENTS }, 'TIKTOK_COMMENTS'); + cItems = cRes.items; + writeFileSync(cCachePath, JSON.stringify(cItems)); + } + const commentsByVid = groupByCanonicalId(reportId, 'TIKTOK_COMMENTS', cItems, idSet); + + mkdirSync(PATHS.enriched(reportId), { recursive: true }); + + const items = ids.map((id) => metaById.get(id)).filter((m): m is Pass1Video => !!m); + const droppedExtras: DroppedVideo[] = []; + for (const id of ids) { + if (!metaById.has(id)) { + droppedExtras.push({ id, reason: 'pass1 record missing' }); + } + } + + const results = await inFlight(items, MAX_CONCURRENCY, (meta) => + processOneVideo({ reportId, meta, transcripts, commentsByVid }), + ); + + const bundled = results.filter((r) => !!r.bundle).length; + const dropped = [...droppedExtras, ...results.filter((r) => r.dropped).map((r) => r.dropped!)]; + + // Persist dropped log + const droppedPath = `${PATHS.pass2(reportId)}/dropped_videos.json`; + writeFileSync(droppedPath, JSON.stringify(dropped, null, 2)); + + console.log(`[stage 4] done. attempted=${ids.length} bundled=${bundled} dropped=${dropped.length} drift=${getDriftCount()}`); + return { + ok: true, + outputs: { + enriched_dir: PATHS.enriched(reportId), + dropped: droppedPath, + drift_log: PATHS.driftLog(reportId), + }, + total_attempted: ids.length, + total_bundled: bundled, + total_dropped: dropped.length, + drift_events: getDriftCount(), + }; +} + diff --git a/v2/pipeline/stages/stage_5_manifest.ts b/v2/pipeline/stages/stage_5_manifest.ts new file mode 100644 index 0000000..a2e7902 --- /dev/null +++ b/v2/pipeline/stages/stage_5_manifest.ts @@ -0,0 +1,114 @@ +// Stage 5: validate every selected video has every required asset. +// Hard gate: throws HardGateError if coverage_pct < 100. +// Auto-backfill on { dropFailing: true }: read pass1 ranking, mark failing +// ids dropped, walk next-best ids, re-run Stage 4 just for those, re-validate. +import { writeFileSync, readFileSync, existsSync } from 'node:fs'; +import { buildManifest, writeManifest, HardGateError, type Manifest } from '../lib/manifest.js'; +import { PATHS } from '../lib/paths.js'; +import { runStage4Pass2Enrich } from './stage_4_pass2_enrich.js'; +import type { Pass1Video } from './stage_2_pass1_scrape.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; + +export interface Stage5Args { + reportId: string; + brief: BriefInput; + dropFailing?: boolean; + /** stop after N backfill rounds (safety). default 3. */ + maxBackfillRounds?: number; +} + +export interface Stage5Result { + ok: boolean; + manifest: Manifest; + passed: boolean; + backfill_rounds: number; + backfilled_ids: string[]; + outputs: Record; +} + +export async function runStage5Manifest(args: Stage5Args): Promise { + const { reportId, brief, dropFailing = false, maxBackfillRounds = 3 } = args; + + const selectedPath = PATHS.selectedIds(reportId); + const pass1Path = PATHS.pass1Videos(reportId); + if (!existsSync(selectedPath)) throw new Error(`selected_video_ids.json missing. Run select first.`); + if (!existsSync(pass1Path)) throw new Error(`pass1_videos.json missing. Run scrape1 first.`); + + let selected: string[] = JSON.parse(readFileSync(selectedPath, 'utf-8')); + const pass1: Pass1Video[] = JSON.parse(readFileSync(pass1Path, 'utf-8')); + const ranked = pass1.map((v) => v.id); + + let manifest = buildManifest(reportId, selected); + let rounds = 0; + const backfilled: string[] = []; + + while (manifest.summary.coverage_pct < 100 && dropFailing && rounds < maxBackfillRounds) { + rounds++; + const failingIds = new Set(manifest.videos.filter((v) => !v.all_ok).map((v) => v.id)); + const survivors = selected.filter((id) => !failingIds.has(id)); + const droppedNow = [...failingIds]; + console.log(`[stage 5] backfill round ${rounds}: dropping ${droppedNow.length}, finding replacements`); + + // Walk next-best ids from pass1, excluding already-selected and already-dropped. + const seen = new Set([...survivors, ...failingIds]); + const candidates: string[] = []; + for (const id of ranked) { + if (seen.has(id)) continue; + candidates.push(id); + if (candidates.length >= droppedNow.length) break; + } + if (candidates.length === 0) { + console.warn(`[stage 5] no backfill candidates left after round ${rounds}; manifest will not be 100%.`); + selected = survivors; + writeFileSync(selectedPath, JSON.stringify(selected, null, 2)); + break; + } + + backfilled.push(...candidates); + // Run Stage 4 just for the new candidates (don't re-enrich survivors). + await runStage4Pass2Enrich({ reportId, brief, onlyIds: candidates }); + + selected = [...survivors, ...candidates]; + writeFileSync(selectedPath, JSON.stringify(selected, null, 2)); + manifest = buildManifest(reportId, selected); + } + + // Always write the latest manifest, regardless of pass/fail. + const manifestPath = writeManifest(reportId, manifest); + + // Backfill log + const backfillLogPath = `${PATHS.pass2(reportId)}/backfill_log.json`; + writeFileSync(backfillLogPath, JSON.stringify({ + rounds, + dropped_ids: manifest.videos.filter((v) => !v.all_ok).map((v) => ({ id: v.id, missing: v.missing })), + backfilled_ids: backfilled, + final_selected_count: selected.length, + final_coverage_pct: manifest.summary.coverage_pct, + }, null, 2)); + + const passed = manifest.summary.coverage_pct === 100; + + if (!passed) { + if (dropFailing) { + console.error(`[stage 5] manifest still incomplete after ${rounds} backfill rounds. Coverage ${manifest.summary.coverage_pct}%.`); + } else { + console.error(`[stage 5] manifest incomplete. Coverage ${manifest.summary.coverage_pct}%. Run with --drop-failing to auto-backfill.`); + } + } else { + console.log(`[stage 5] manifest PASS — coverage 100%, ${selected.length} videos ready for analysis.`); + } + + return { + ok: passed, + manifest, + passed, + backfill_rounds: rounds, + backfilled_ids: backfilled, + outputs: { + manifest: manifestPath, + backfill_log: backfillLogPath, + }, + }; +} + +export { HardGateError }; diff --git a/v2/pipeline/stages/stage_6_analyse.ts b/v2/pipeline/stages/stage_6_analyse.ts new file mode 100644 index 0000000..5977d38 --- /dev/null +++ b/v2/pipeline/stages/stage_6_analyse.ts @@ -0,0 +1,163 @@ +// Stage 6 — per-video Claude analysis. Reads bundle.json for each manifest-passing +// video, builds a single prompt with caption + transcript + comments + frame refs, +// emits a structured JSON record per the V3 schema. Cached: skips files that exist. +import { writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { callClaudeJSON } from '../lib/claude.js'; +import { loadRubric } from '../lib/rubrics.js'; +import { loadManifest, loadBundle, HardGateError } from '../lib/manifest.js'; +import { PATHS, ensureDir } from '../lib/paths.js'; +import type { VideoBundle } from './stage_4_pass2_enrich.js'; + +export const ANALYSIS_SCHEMA = z.object({ + id: z.string(), + what_happens: z.string(), + hook: z.object({ + first_3_seconds: z.string(), + pattern: z.string(), + why_it_stops_scroll: z.string(), + }), + visual_aesthetic: z.object({ + lighting: z.string(), + colour_palette: z.array(z.string()), + setting: z.string(), + talent: z.string(), + products_visible: z.array(z.string()).default([]), + on_screen_text_examples: z.array(z.string()).default([]), + }), + format: z.string(), + audio: z.object({ + music_present: z.boolean(), + music_mood: z.string(), + voiceover: z.boolean(), + asmr_elements: z.boolean(), + }), + narrative: z.object({ + thesis: z.string(), + tension: z.string(), + resolution: z.string(), + }), + audience_signals: z.object({ + comment_themes: z.array(z.string()), + comment_sentiment_split: z.object({ positive: z.number(), neutral: z.number(), critical: z.number() }), + verbatim_quotes: z.array(z.object({ text: z.string(), likes: z.number(), theme: z.string() })), + }), + paid_or_organic: z.object({ + label: z.enum(['paid', 'organic', 'unclear']), + reasoning: z.string(), + evidence_signals_used: z.array(z.string()).default([]), + }), +}); + +export type Analysis = z.infer; + +function buildPrompt(bundle: VideoBundle, rubric: string): string { + const m = bundle.metadata; + const meta = [ + `id: ${m.id}`, + `handle: @${m.handle}`, + `plays: ${m.plays.toLocaleString()}, likes: ${m.likes.toLocaleString()}, saves: ${m.saves.toLocaleString()}, comments: ${m.comments_count.toLocaleString()}, shares: ${m.shares.toLocaleString()}`, + `STL%: ${m.stl_pct}, duration: ${m.duration_sec}s, posted: ${m.posted_at}`, + `caption: ${m.caption}`, + `hashtags: ${m.hashtags.join(' ')}`, + ].join('\n'); + + const transcript = bundle.transcript ? bundle.transcript.text_en : '(no transcript)'; + const comments = bundle.comments + .map((c, i) => `${i + 1}. (${c.likes} likes) @${c.author_handle}: ${c.text_en}`) + .join('\n'); + const frameNote = `${bundle.frames.length} frames extracted (1fps cap, 720px wide). Reference them when commenting on visual_aesthetic.`; + + return `${rubric} + +--- + +# Video metadata + +${meta} + +# English transcript + +${transcript} + +# Top comments (English) + +${comments} + +# Frame note + +${frameNote} + +Return the analysis JSON now.`; +} + +async function analyseOne(reportId: string, bundle: VideoBundle, rubric: string): Promise { + const prompt = buildPrompt(bundle, rubric); + const raw = await callClaudeJSON(prompt, { label: `analyse:${bundle.id}`, maxTokens: 4096 }); + const parsed = ANALYSIS_SCHEMA.safeParse(raw); + if (!parsed.success) { + throw new Error(`Analysis JSON failed schema for ${bundle.id}: ${parsed.error.message}`); + } + // Defence: model could echo a different id; force-set ours. + return { ...parsed.data, id: bundle.id }; +} + +const MAX_CONCURRENCY = 4; + +export interface Stage6Result { + ok: true; + outputs: Record; + total: number; + cached: number; + fresh: number; +} + +export async function runStage6Analyse(reportId: string): Promise { + const manifest = loadManifest(reportId); + if (!manifest) throw new Error(`manifest.json missing. Run validate first.`); + if (manifest.summary.coverage_pct < 100) { + throw new HardGateError( + `manifest coverage ${manifest.summary.coverage_pct}% — refusing to start analysis`, + manifest, + ); + } + const passing = manifest.videos.filter((v) => v.all_ok).map((v) => v.id); + const rubric = loadRubric('per_video_analysis'); + ensureDir(PATHS.analysisDir(reportId)); + + let cached = 0; + let fresh = 0; + + async function workOne(id: string): Promise { + const out = join(PATHS.analysisDir(reportId), `${id}.json`); + if (existsSync(out)) { + try { ANALYSIS_SCHEMA.parse(JSON.parse(readFileSync(out, 'utf-8'))); cached++; return; } + catch { /* re-analyse */ } + } + const bundle = loadBundle(reportId, id); + if (!bundle) throw new Error(`bundle.json missing for ${id}`); + const analysis = await analyseOne(reportId, bundle, rubric); + writeFileSync(out, JSON.stringify(analysis, null, 2)); + fresh++; + } + + // Concurrency-limited sweep. + let i = 0; + async function worker(): Promise { + while (i < passing.length) { + const idx = i++; + const id = passing[idx]!; + try { await workOne(id); } + catch (err) { console.error(`[stage 6] ${id} failed: ${(err as Error).message}`); throw err; } + } + } + await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENCY, passing.length) }, () => worker())); + + return { + ok: true, + outputs: { analyses_dir: PATHS.analysisDir(reportId) }, + total: passing.length, + cached, fresh, + }; +} diff --git a/v2/pipeline/stages/stage_7_atomic_insights.ts b/v2/pipeline/stages/stage_7_atomic_insights.ts new file mode 100644 index 0000000..65d90b5 --- /dev/null +++ b/v2/pipeline/stages/stage_7_atomic_insights.ts @@ -0,0 +1,143 @@ +// Stage 7 — turn per-video analyses into 200–500 atomic insights. +// Iterates analyses in batches of 20; each batch sees the running list so it can +// extend existing atomic_ids rather than duplicate. +import { writeFileSync, readFileSync, readdirSync, existsSync } from 'node:fs'; +import { z } from 'zod'; +import { callClaudeJSON } from '../lib/claude.js'; +import { loadRubric } from '../lib/rubrics.js'; +import { PATHS } from '../lib/paths.js'; +import { ANALYSIS_SCHEMA, type Analysis } from './stage_6_analyse.js'; + +const TYPE = z.enum(['hook', 'visual', 'audio', 'narrative']); + +const BATCH_SCHEMA = z.object({ + additions: z.array(z.object({ + atomic_id: z.string(), + type: TYPE, + observation: z.string().min(8), + supporting_video_ids: z.array(z.string()).min(1), + })).default([]), + extensions: z.array(z.object({ + atomic_id: z.string(), + added_video_ids: z.array(z.string()).min(1), + })).default([]), +}); + +export interface AtomicInsight { + atomic_id: string; // "ATM-0001" + type: 'hook' | 'visual' | 'audio' | 'narrative'; + observation: string; + supporting_video_ids: string[]; + frequency: number; // = supporting_video_ids.length, single source of truth +} + +const BATCH_SIZE = 20; + +function nextId(n: number): string { + return `ATM-${String(n).padStart(4, '0')}`; +} + +function loadAllAnalyses(reportId: string): Analysis[] { + const dir = PATHS.analysisDir(reportId); + if (!existsSync(dir)) return []; + const files = readdirSync(dir).filter((f) => f.endsWith('.json')); + const out: Analysis[] = []; + for (const f of files) { + try { + const data = JSON.parse(readFileSync(`${dir}/${f}`, 'utf-8')); + const parsed = ANALYSIS_SCHEMA.safeParse(data); + if (parsed.success) out.push(parsed.data); + } catch { /* skip malformed */ } + } + return out; +} + +function summariseAnalysis(a: Analysis): string { + return [ + `### Video ${a.id} (@${''})`, + `format: ${a.format}; what_happens: ${a.what_happens}`, + `hook: pattern=${a.hook.pattern}; first_3s="${a.hook.first_3_seconds.replace(/\n/g, ' ').slice(0, 200)}"`, + `visual: lighting=${a.visual_aesthetic.lighting}, setting=${a.visual_aesthetic.setting}, talent=${a.visual_aesthetic.talent}, on_screen_text=${a.visual_aesthetic.on_screen_text_examples.slice(0, 3).join(' | ')}`, + `audio: music=${a.audio.music_present} mood=${a.audio.music_mood} voiceover=${a.audio.voiceover} asmr=${a.audio.asmr_elements}`, + `narrative: thesis="${a.narrative.thesis.slice(0, 200)}"; tension="${a.narrative.tension.slice(0, 150)}"`, + `audience: themes=${a.audience_signals.comment_themes.slice(0, 5).join(' | ')}`, + `paid_or_organic: ${a.paid_or_organic.label}`, + ].join('\n'); +} + +function summariseRunningList(insights: AtomicInsight[], cap = 80): string { + const lines = insights.slice(-cap).map((i) => `${i.atomic_id} [${i.type}, freq ${i.frequency}] ${i.observation}`); + return lines.length === 0 ? '(no atomic insights yet)' : lines.join('\n'); +} + +export interface Stage7Result { + ok: true; + outputs: Record; + total_insights: number; + by_type: Record<'hook' | 'visual' | 'audio' | 'narrative', number>; +} + +export async function runStage7AtomicInsights(reportId: string): Promise { + const analyses = loadAllAnalyses(reportId); + if (analyses.length === 0) throw new Error('No analyses found. Run stage 6 first.'); + + const rubric = loadRubric('atomic_insights'); + const insights: AtomicInsight[] = []; + const byId = new Map(); + let counter = 0; + + for (let start = 0; start < analyses.length; start += BATCH_SIZE) { + const batch = analyses.slice(start, start + BATCH_SIZE); + const summaries = batch.map(summariseAnalysis).join('\n\n'); + const running = summariseRunningList(insights); + const prompt = `${rubric} + +--- + +# Existing atomic insights (running list, truncated to recent ${insights.length} entries) + +${running} + +# This batch (${batch.length} videos) + +${summaries} + +Return the JSON now.`; + + const raw = await callClaudeJSON(prompt, { label: `atomic_insights:batch_${start}`, maxTokens: 8192 }); + const parsed = BATCH_SCHEMA.safeParse(raw); + if (!parsed.success) { + console.warn(`[stage 7] batch starting at ${start} returned bad shape: ${parsed.error.message.slice(0, 200)}; skipping.`); + continue; + } + for (const a of parsed.data.additions) { + counter += 1; + const id = nextId(counter); + const item: AtomicInsight = { + atomic_id: id, + type: a.type, + observation: a.observation.trim(), + supporting_video_ids: [...new Set(a.supporting_video_ids)], + frequency: 0, + }; + item.frequency = item.supporting_video_ids.length; + insights.push(item); + byId.set(id, item); + } + for (const ext of parsed.data.extensions) { + const target = byId.get(ext.atomic_id); + if (!target) continue; + const merged = new Set([...target.supporting_video_ids, ...ext.added_video_ids]); + target.supporting_video_ids = [...merged]; + target.frequency = target.supporting_video_ids.length; + } + console.log(`[stage 7] batch ${start / BATCH_SIZE + 1}: +${parsed.data.additions.length} additions, ${parsed.data.extensions.length} extensions (total ${insights.length})`); + } + + const byType: Stage7Result['by_type'] = { hook: 0, visual: 0, audio: 0, narrative: 0 }; + for (const i of insights) byType[i.type]++; + + const out = PATHS.atomicInsights(reportId); + writeFileSync(out, JSON.stringify(insights, null, 2)); + return { ok: true, outputs: { atomic_insights: out }, total_insights: insights.length, by_type: byType }; +} diff --git a/v2/pipeline/stages/stage_8_trends.ts b/v2/pipeline/stages/stage_8_trends.ts new file mode 100644 index 0000000..3068b22 --- /dev/null +++ b/v2/pipeline/stages/stage_8_trends.ts @@ -0,0 +1,293 @@ +// Stage 8 — trend synthesis. Three sub-steps: +// 8a: brief-driven categories (§4.5d). +// 8b: cluster atomic insights into trends (§4.5e naming). +// 8b.5: business_question_relevance per trend (§4.5c calibration anchors). Drop <0.35. +import { writeFileSync, readFileSync, readdirSync, existsSync } from 'node:fs'; +import { z } from 'zod'; +import { callClaudeJSON } from '../lib/claude.js'; +import { loadRubric } from '../lib/rubrics.js'; +import { PATHS } from '../lib/paths.js'; +import type { AtomicInsight } from './stage_7_atomic_insights.js'; +import type { BriefInput } from '../../server/schemas/brief.js'; +import { ANALYSIS_SCHEMA, type Analysis } from './stage_6_analyse.js'; + +// ─── 8a categories ─── +const CATEGORIES_SCHEMA = z.object({ + categories: z.array(z.object({ name: z.string().min(2), rationale: z.string() })).min(3), + rejected: z.array(z.object({ name: z.string(), reason: z.string() })).default([]), +}); +type Categories = z.infer; + +// ─── 8b trends (raw from Claude) ─── +// V3 brief mandates ≥5 supporting videos per trend in production. Override via env +// for small corpora (smoke tests, brand-new accounts) where 5 isn't always reachable. +const MIN_SUPPORTING = parseInt(process.env.MIN_SUPPORTING_VIDEOS_PER_TREND ?? '5', 10); +const RAW_TRENDS_SCHEMA = z.object({ + trends: z.array(z.object({ + slug: z.string().min(2), + name: z.string().min(3), + category: z.string().min(1), + narrative: z.string().min(20), + lens_tags: z.array(z.enum(['hooks', 'visual', 'audio', 'sentiment', 'narrative'])).min(1), + top_atomic_ids: z.array(z.string()).default([]), + supporting_video_ids: z.array(z.string()).min(MIN_SUPPORTING), + })).min(1), +}); + +// ─── 8b.5 relevance ─── +const RELEVANCE_SCHEMA = z.object({ + score: z.number().min(0).max(1), + tier: z.enum(['core', 'peripheral', 'dropped']), + justification: z.string(), +}); +type Relevance = z.infer; + +// ─── Trend final shape (what we write to trends.json) ─── +export interface TrendKpis { + plays_total: number; + videos: number; + unique_creators: number; + avg_stl_pct: number; + paid_organic_split: { paid: number; organic: number; unclear: number }; +} + +export interface Trend { + trend_id: string; // "TR-001" + slug: string; + name: string; + category: string; + narrative: string; + lens_tags: string[]; + top_atomic_ids: string[]; + supporting_video_ids: string[]; + business_question_relevance: Relevance; + kpis: TrendKpis; +} + +function loadAnalyses(reportId: string): Map { + const dir = PATHS.analysisDir(reportId); + const map = new Map(); + if (!existsSync(dir)) return map; + for (const f of readdirSync(dir)) { + if (!f.endsWith('.json')) continue; + try { + const data = JSON.parse(readFileSync(`${dir}/${f}`, 'utf-8')); + const parsed = ANALYSIS_SCHEMA.safeParse(data); + if (parsed.success) map.set(parsed.data.id, parsed.data); + } catch { /* skip */ } + } + return map; +} + +function briefBlock(brief: BriefInput): string { + return [ + `Brand: ${brief.brand.name} (@${brief.brand.handle})${brief.brand.positioning ? ` — ${brief.brand.positioning}` : ''}`, + `Category: ${brief.category}`, + `Audience: ${brief.audience.primary} (${brief.audience.age_range} ${brief.audience.gender})`, + `Geo/Language: ${brief.geo} / ${brief.language}`, + `Business question: ${brief.business_question}`, + `KPIs: ${brief.kpis.join(' | ')}`, + brief.context_vision ? `Context: ${brief.context_vision}` : '', + ].filter(Boolean).join('\n'); +} + +async function generateCategories(brief: BriefInput, atomicSummary: string): Promise { + const rubric = loadRubric('category_quality'); + const prompt = `${rubric} + +--- + +# Brief + +${briefBlock(brief)} + +# Atomic insights summary + +${atomicSummary} + +Generate the categories JSON now.`; + const raw = await callClaudeJSON(prompt, { label: 'stage_8a_categories', maxTokens: 4096 }); + return CATEGORIES_SCHEMA.parse(raw); +} + +function summariseAtomicsForPrompt(insights: AtomicInsight[], cap = 200): string { + // Group by type and take top by frequency to keep the prompt reasonable. + const byType: Record = { hook: [], visual: [], audio: [], narrative: [] }; + for (const i of insights) (byType[i.type] ??= []).push(i); + for (const k of Object.keys(byType)) { + byType[k]!.sort((a, b) => b.frequency - a.frequency); + } + const lines: string[] = []; + const perTypeCap = Math.max(20, Math.floor(cap / 4)); + for (const t of ['hook', 'visual', 'audio', 'narrative']) { + lines.push(`## ${t.toUpperCase()}`); + for (const i of (byType[t] ?? []).slice(0, perTypeCap)) { + lines.push(`- ${i.atomic_id} [freq ${i.frequency}] ${i.observation} (videos: ${i.supporting_video_ids.slice(0, 6).join(', ')}${i.supporting_video_ids.length > 6 ? ', …' : ''})`); + } + } + return lines.join('\n'); +} + +async function clusterTrends(brief: BriefInput, categoryNames: string[], atomicSummary: string): Promise> { + const rubric = loadRubric('trend_synthesis'); + const namingRubric = loadRubric('editorial_naming'); + const prompt = `${rubric} + +--- + +${namingRubric} + +--- + +# Brief + +${briefBlock(brief)} + +# Categories chosen for this report + +${categoryNames.map((c, i) => `${i + 1}. ${c}`).join('\n')} + +# Atomic insights (top by frequency, by type) + +${atomicSummary} + +Cluster atomic insights into trends now. Aim for 50 trends; floor 35; never split weak trends. Return ONLY the JSON.`; + const raw = await callClaudeJSON(prompt, { label: 'stage_8b_trends', maxTokens: 16384 }); + return RAW_TRENDS_SCHEMA.parse(raw); +} + +async function scoreRelevance(brief: BriefInput, trendName: string, narrative: string): Promise { + const anchors = loadRubric('relevance_calibration'); + const prompt = `${anchors} + +--- + +# Brief business question + +${brief.business_question} + +# Trend to score + +NAME: ${trendName} +NARRATIVE: ${narrative} + +Score it now. Return ONLY the JSON.`; + const raw = await callClaudeJSON(prompt, { label: `stage_8b5_relevance:${trendName.slice(0, 40)}`, maxTokens: 1024 }); + return RELEVANCE_SCHEMA.parse(raw); +} + +function computeKpis(trend: { supporting_video_ids: string[] }, analyses: Map, pass1: Map): TrendKpis { + let plays = 0, sumStl = 0, n = 0, paid = 0, organic = 0, unclear = 0; + const creators = new Set(); + for (const id of trend.supporting_video_ids) { + const meta = pass1.get(id); + if (!meta) continue; + plays += meta.plays; + sumStl += meta.stl_pct; + n += 1; + creators.add(meta.handle); + const a = analyses.get(id); + if (a) { + if (a.paid_or_organic.label === 'paid') paid++; + else if (a.paid_or_organic.label === 'organic') organic++; + else unclear++; + } else { + unclear++; + } + } + return { + plays_total: plays, + videos: trend.supporting_video_ids.length, + unique_creators: creators.size, + avg_stl_pct: n === 0 ? 0 : Math.round((sumStl / n) * 100) / 100, + paid_organic_split: { paid, organic, unclear }, + }; +} + +export interface Stage8Result { + ok: true; + outputs: Record; + categories: string[]; + total_trends: number; + core_trends: number; + peripheral_trends: number; + dropped_trends: number; +} + +export async function runStage8Trends(reportId: string, brief: BriefInput): Promise { + // Load atomic insights + const atomicsPath = PATHS.atomicInsights(reportId); + if (!existsSync(atomicsPath)) throw new Error('atomic_insights.json missing. Run stage 7 first.'); + const insights: AtomicInsight[] = JSON.parse(readFileSync(atomicsPath, 'utf-8')); + if (insights.length === 0) throw new Error('No atomic insights to synthesise from.'); + + // Load per-video analyses + pass1 for KPI computation + const analyses = loadAnalyses(reportId); + const pass1Path = PATHS.pass1Videos(reportId); + type LiteMeta = { plays: number; likes: number; saves: number; comments_count: number; shares: number; stl_pct: number; handle: string }; + const pass1Lite = new Map(); + if (existsSync(pass1Path)) { + const arr = JSON.parse(readFileSync(pass1Path, 'utf-8')) as Array<{ id: string } & LiteMeta>; + for (const v of arr) pass1Lite.set(v.id, v); + } + + // 8a — categories + const atomicSummary = summariseAtomicsForPrompt(insights); + const categories = await generateCategories(brief, atomicSummary); + writeFileSync(PATHS.categories(reportId), JSON.stringify(categories, null, 2)); + const categoryNames = categories.categories.map((c) => c.name); + console.log(`[stage 8a] ${categoryNames.length} categories: ${categoryNames.join(', ')}`); + + // 8b — cluster + const rawTrends = await clusterTrends(brief, categoryNames, atomicSummary); + + // 8b.5 — relevance scoring + filter + const finalTrends: Trend[] = []; + let dropped = 0, core = 0, peripheral = 0; + for (let i = 0; i < rawTrends.trends.length; i++) { + const r = rawTrends.trends[i]!; + let relevance: Relevance; + try { + relevance = await scoreRelevance(brief, r.name, r.narrative); + } catch (err) { + console.warn(`[stage 8b.5] relevance scoring failed for "${r.name}": ${(err as Error).message}; defaulting to peripheral 0.5`); + relevance = { score: 0.5, tier: 'peripheral', justification: 'scoring failed; defaulted' }; + } + if (relevance.score < 0.35) { dropped++; continue; } + if (relevance.tier === 'core' || relevance.score >= 0.6) core++; else peripheral++; + const trendId = `TR-${String(finalTrends.length + 1).padStart(3, '0')}`; + const kpis = computeKpis({ supporting_video_ids: r.supporting_video_ids }, analyses, pass1Lite); + finalTrends.push({ + trend_id: trendId, + slug: r.slug, + name: r.name, + category: r.category, + narrative: r.narrative, + lens_tags: r.lens_tags, + top_atomic_ids: r.top_atomic_ids, + supporting_video_ids: r.supporting_video_ids, + business_question_relevance: relevance, + kpis, + }); + } + + writeFileSync(PATHS.trends(reportId), JSON.stringify(finalTrends, null, 2)); + + console.log(`[stage 8] ${finalTrends.length} trends (core=${core}, peripheral=${peripheral}, dropped=${dropped})`); + if (finalTrends.length < 35) { + console.warn(`[stage 8] WARNING: only ${finalTrends.length} trends (floor 35). Brief may be too narrow or atomic insights too thin.`); + } + + return { + ok: true, + outputs: { + categories: PATHS.categories(reportId), + trends: PATHS.trends(reportId), + }, + categories: categoryNames, + total_trends: finalTrends.length, + core_trends: core, + peripheral_trends: peripheral, + dropped_trends: dropped, + }; +} diff --git a/v2/pipeline/stages/stage_9_qa.ts b/v2/pipeline/stages/stage_9_qa.ts new file mode 100644 index 0000000..f484e81 --- /dev/null +++ b/v2/pipeline/stages/stage_9_qa.ts @@ -0,0 +1,122 @@ +// Stage 9 — QA gates. +// +// 9a (automated): aggregate paid/organic flags per creator. +// 9b (automated): re-validate manifest coverage = 100%. +// 9c, 9d (human): CM + Strategist checklists are surfaced in the operator UI; +// the orchestrator just records signoffs and refuses to advance without both. +import { writeFileSync, readFileSync, readdirSync, existsSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { ANALYSIS_SCHEMA, type Analysis } from './stage_6_analyse.js'; +import { loadManifest, HardGateError } from '../lib/manifest.js'; +import { PATHS } from '../lib/paths.js'; + +interface PaidOrganicCreator { + handle: string; + videos_in_report: number; + paid_videos: number; + organic_videos: number; + unclear_videos: number; + label: 'paid' | 'organic' | 'mixed' | 'unclear'; + evidence_signals: string[]; + needs_human_confirm: boolean; +} + +function aggregatePaidOrganic(analyses: Analysis[], handleByVideoId: Map): PaidOrganicCreator[] { + const byHandle = new Map(); + for (const a of analyses) { + const handle = handleByVideoId.get(a.id) || 'unknown'; + let row = byHandle.get(handle); + if (!row) { + row = { + handle, + videos_in_report: 0, + paid_videos: 0, organic_videos: 0, unclear_videos: 0, + label: 'unclear', + evidence_signals: [], + needs_human_confirm: false, + }; + byHandle.set(handle, row); + } + row.videos_in_report++; + if (a.paid_or_organic.label === 'paid') row.paid_videos++; + else if (a.paid_or_organic.label === 'organic') row.organic_videos++; + else row.unclear_videos++; + for (const sig of a.paid_or_organic.evidence_signals_used) { + if (!row.evidence_signals.includes(sig)) row.evidence_signals.push(sig); + } + } + for (const row of byHandle.values()) { + if (row.paid_videos > 0 && row.organic_videos > 0) row.label = 'mixed'; + else if (row.paid_videos > 0) row.label = 'paid'; + else if (row.organic_videos > 0) row.label = 'organic'; + else row.label = 'unclear'; + row.needs_human_confirm = row.paid_videos > 0 || row.label === 'mixed'; + } + return [...byHandle.values()].sort((a, b) => b.videos_in_report - a.videos_in_report); +} + +export interface Stage9Result { + ok: boolean; + outputs: Record; + paid_creators: number; + mixed_creators: number; + coverage_pct: number; +} + +export async function runStage9Qa(reportId: string): Promise { + // 9b — coverage check first (cheaper, fail fast). + const manifest = loadManifest(reportId); + if (!manifest) throw new Error('manifest.json missing. Run validate first.'); + if (manifest.summary.coverage_pct < 100) { + throw new HardGateError(`coverage ${manifest.summary.coverage_pct}% — refusing QA`, manifest); + } + + // 9a — paid/organic aggregation + const dir = PATHS.analysisDir(reportId); + const analyses: Analysis[] = []; + if (existsSync(dir)) { + for (const f of readdirSync(dir)) { + if (!f.endsWith('.json')) continue; + try { + const data = JSON.parse(readFileSync(join(dir, f), 'utf-8')); + const parsed = ANALYSIS_SCHEMA.safeParse(data); + if (parsed.success) analyses.push(parsed.data); + } catch { /* skip */ } + } + } + + // Build handle map from pass1 records (analyses don't carry handle). + const pass1Path = PATHS.pass1Videos(reportId); + const handleById = new Map(); + if (existsSync(pass1Path)) { + const pass1 = JSON.parse(readFileSync(pass1Path, 'utf-8')) as Array<{ id: string; handle: string }>; + for (const v of pass1) handleById.set(v.id, v.handle); + } + + const creators = aggregatePaidOrganic(analyses, handleById); + mkdirSync(PATHS.qaDir(reportId), { recursive: true }); + const paidPath = join(PATHS.qaDir(reportId), 'paid_organic_review.json'); + writeFileSync(paidPath, JSON.stringify(creators, null, 2)); + + // 9b — record coverage check artefact alongside. + const coveragePath = join(PATHS.qaDir(reportId), 'coverage_check.json'); + writeFileSync(coveragePath, JSON.stringify({ + passed: true, + coverage_pct: manifest.summary.coverage_pct, + selected_count: manifest.selected_count, + all_ok_count: manifest.summary.all_ok, + checked_at: new Date().toISOString(), + }, null, 2)); + + const paid = creators.filter((c) => c.label === 'paid').length; + const mixed = creators.filter((c) => c.label === 'mixed').length; + console.log(`[stage 9] paid creators: ${paid}, mixed: ${mixed}, coverage: ${manifest.summary.coverage_pct}%`); + + return { + ok: true, + outputs: { paid_organic_review: paidPath, coverage_check: coveragePath }, + paid_creators: paid, + mixed_creators: mixed, + coverage_pct: manifest.summary.coverage_pct, + }; +} diff --git a/v2/server/__tests__/auth.test.ts b/v2/server/__tests__/auth.test.ts new file mode 100644 index 0000000..9da5170 --- /dev/null +++ b/v2/server/__tests__/auth.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { roleAtLeast, type TeamRole } from '../db/memberships.js'; + +describe('roleAtLeast', () => { + it('owner satisfies every requirement', () => { + for (const r of ['owner', 'admin', 'editor', 'viewer'] as TeamRole[]) { + expect(roleAtLeast('owner', r)).toBe(true); + } + }); + + it('viewer cannot edit', () => { + expect(roleAtLeast('viewer', 'editor')).toBe(false); + expect(roleAtLeast('viewer', 'admin')).toBe(false); + expect(roleAtLeast('viewer', 'owner')).toBe(false); + }); + + it('editor can view and edit but not admin', () => { + expect(roleAtLeast('editor', 'viewer')).toBe(true); + expect(roleAtLeast('editor', 'editor')).toBe(true); + expect(roleAtLeast('editor', 'admin')).toBe(false); + }); + + it('admin can manage team but cannot do owner-only ops', () => { + expect(roleAtLeast('admin', 'editor')).toBe(true); + expect(roleAtLeast('admin', 'admin')).toBe(true); + expect(roleAtLeast('admin', 'owner')).toBe(false); + }); +}); diff --git a/v2/server/__tests__/session.test.ts b/v2/server/__tests__/session.test.ts new file mode 100644 index 0000000..75dc2b5 --- /dev/null +++ b/v2/server/__tests__/session.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeAll } from 'vitest'; + +beforeAll(() => { + process.env.SESSION_SECRET = 'test-secret-deterministic-for-tests-only'; +}); + +describe('session sign/verify roundtrip', () => { + it('issues a token that getSession can recover', async () => { + const { issueSession, COOKIE_NAME } = await import('../auth/session.js'); + const token = issueSession({ + user_id: '11111111-1111-1111-1111-111111111111', + email: 'alice@example.com', + active_team_id: '22222222-2222-2222-2222-222222222222', + auth_method: 'azure-sso', + }); + + const fakeReq = { headers: { cookie: `${COOKIE_NAME}=${token}` } } as unknown as Parameters[0]; + const { getSession } = await import('../auth/session.js'); + const session = getSession(fakeReq); + + expect(session).not.toBeNull(); + expect(session?.email).toBe('alice@example.com'); + expect(session?.active_team_id).toBe('22222222-2222-2222-2222-222222222222'); + }); + + it('rejects a tampered token', async () => { + const { issueSession, COOKIE_NAME, getSession } = await import('../auth/session.js'); + const token = issueSession({ + user_id: '11111111-1111-1111-1111-111111111111', + email: 'alice@example.com', + active_team_id: '22222222-2222-2222-2222-222222222222', + auth_method: 'azure-sso', + }); + const tampered = token.replace(/^./, (c) => (c === 'a' ? 'b' : 'a')); + const fakeReq = { headers: { cookie: `${COOKIE_NAME}=${tampered}` } } as unknown as Parameters[0]; + expect(getSession(fakeReq)).toBeNull(); + }); + + it('returns null when no cookie is present', async () => { + const { getSession } = await import('../auth/session.js'); + const fakeReq = { headers: {} } as unknown as Parameters[0]; + expect(getSession(fakeReq)).toBeNull(); + }); +}); diff --git a/v2/server/auth/jwks.ts b/v2/server/auth/jwks.ts new file mode 100644 index 0000000..ad965ef --- /dev/null +++ b/v2/server/auth/jwks.ts @@ -0,0 +1,103 @@ +// Lifted from V1 agents/social-listening/dashboard/server.ts:127-194. +// Azure AD ID-token validation: JWKS fetch with 24h cache + RSA-SHA256 signature check. +import { createPublicKey, createVerify } from 'node:crypto'; +import { envStr } from '../lib/env.js'; + +const TENANT_ID = envStr('AZURE_TENANT_ID'); +const CLIENT_ID = envStr('AZURE_CLIENT_ID'); + +export const SSO_ENABLED = !!(TENANT_ID && CLIENT_ID); + +let jwksCache: { keys: Record[]; fetchedAt: number } | null = null; +const JWKS_CACHE_TTL = 24 * 60 * 60 * 1000; + +async function getSigningKeys(): Promise[]> { + if (jwksCache && Date.now() - jwksCache.fetchedAt < JWKS_CACHE_TTL) { + return jwksCache.keys; + } + const url = `https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys`; + const resp = await fetch(url); + if (!resp.ok) throw new Error(`JWKS fetch failed: ${resp.status}`); + const data = await resp.json() as { keys: Record[] }; + jwksCache = { keys: data.keys, fetchedAt: Date.now() }; + return data.keys; +} + +function base64urlDecode(str: string): Buffer { + return Buffer.from(str.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); +} + +export interface AzureClaims { + oid: string; + sub: string; + email?: string; + preferred_username?: string; + name?: string; + tid: string; + aud: string; + iss: string; + exp: number; + nbf?: number; + [key: string]: unknown; +} + +export interface VerifyResult { + valid: boolean; + claims?: AzureClaims; + error?: string; +} + +export async function verifyAzureIdToken(idToken: string): Promise { + if (!SSO_ENABLED) return { valid: false, error: 'SSO not configured' }; + const parts = idToken.split('.'); + if (parts.length !== 3) return { valid: false, error: 'Malformed JWT' }; + const headerB64 = parts[0]!; + const payloadB64 = parts[1]!; + const signatureB64 = parts[2]!; + + let header: Record, payload: AzureClaims; + try { + header = JSON.parse(base64urlDecode(headerB64).toString()); + payload = JSON.parse(base64urlDecode(payloadB64).toString()) as AzureClaims; + } catch { + return { valid: false, error: 'Invalid JWT encoding' }; + } + + if (payload.aud !== CLIENT_ID) return { valid: false, error: 'Invalid audience' }; + if (payload.iss !== `https://login.microsoftonline.com/${TENANT_ID}/v2.0`) { + return { valid: false, error: 'Invalid issuer' }; + } + const now = Math.floor(Date.now() / 1000); + if (typeof payload.exp === 'number' && payload.exp < now - 300) { + return { valid: false, error: 'Token expired' }; + } + if (typeof payload.nbf === 'number' && payload.nbf > now + 300) { + return { valid: false, error: 'Token not yet valid' }; + } + + let keys = await getSigningKeys(); + let key = keys.find((k) => k.kid === header.kid); + if (!key) { + jwksCache = null; + keys = await getSigningKeys(); + key = keys.find((k) => k.kid === header.kid); + if (!key) return { valid: false, error: 'Signing key not found' }; + } + + try { + const publicKey = createPublicKey({ + key: { kty: key.kty, n: key.n, e: key.e }, + format: 'jwk', + }); + const verifier = createVerify('RSA-SHA256'); + verifier.update(`${headerB64}.${payloadB64}`); + if (!verifier.verify(publicKey, base64urlDecode(signatureB64))) { + return { valid: false, error: 'Invalid signature' }; + } + } catch (err) { + return { valid: false, error: `Signature verification error: ${(err as Error).message}` }; + } + + if (!payload.oid) return { valid: false, error: 'Token missing oid claim' }; + return { valid: true, claims: payload }; +} diff --git a/v2/server/auth/password-fallback.ts b/v2/server/auth/password-fallback.ts new file mode 100644 index 0000000..9ce0e99 --- /dev/null +++ b/v2/server/auth/password-fallback.ts @@ -0,0 +1,74 @@ +// Emergency password login. OFF in prod by default. Lifted from V1 dashboard/server.ts:351-386 +// but adapted to V2: still upserts a user row keyed on a deterministic synthetic oid so the +// same DASH_USER lands in the users table consistently. +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { envBool, envStr } from '../lib/env.js'; +import { parseJSONBody, sendJSON, clientIp } from '../lib/http.js'; +import { issueSession, setSessionCookie } from './session.js'; +import { landSsoUser } from './upsert-user.js'; + +const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; +const RATE_LIMIT_MAX = 5; +const attempts = new Map(); + +function isRateLimited(ip: string): boolean { + const r = attempts.get(ip); + if (!r) return false; + if (Date.now() - r.firstAt > RATE_LIMIT_WINDOW_MS) { attempts.delete(ip); return false; } + return r.count >= RATE_LIMIT_MAX; +} + +function recordAttempt(ip: string): void { + const now = Date.now(); + const r = attempts.get(ip); + if (!r || now - r.firstAt > RATE_LIMIT_WINDOW_MS) { + attempts.set(ip, { count: 1, firstAt: now }); + } else { + r.count++; + } +} + +export async function handlePasswordLogin(req: IncomingMessage, res: ServerResponse): Promise { + if (!envBool('ALLOW_PASSWORD_FALLBACK', false)) { + sendJSON(res, 404, { ok: false, error: 'Password login not enabled' }); + return; + } + const ip = clientIp(req); + if (isRateLimited(ip)) { + sendJSON(res, 429, { ok: false, error: 'Too many attempts. Try again in 15 minutes.' }); + return; + } + const DASH_USER = envStr('DASH_USER'); + const DASH_PASS = envStr('DASH_PASS'); + if (!DASH_USER || !DASH_PASS) { + sendJSON(res, 503, { ok: false, error: 'Password login misconfigured' }); + return; + } + + let username = '', password = ''; + try { + const body = await parseJSONBody<{ username?: string; password?: string }>(req); + username = body.username ?? ''; + password = body.password ?? ''; + } catch { sendJSON(res, 400, { ok: false, error: 'Invalid body' }); return; } + + if (username !== DASH_USER || password !== DASH_PASS) { + recordAttempt(ip); + sendJSON(res, 401, { ok: false, error: 'Invalid username or password' }); + return; + } + attempts.delete(ip); + + // Land the password user the same way SSO users land — deterministic synthetic oid. + const oid = `pw:${DASH_USER}`; + const email = `${DASH_USER}@local.password-fallback`; + const landed = await landSsoUser({ oid, email, display_name: DASH_USER }); + const token = issueSession({ + user_id: landed.user.id, + email: landed.user.email, + active_team_id: landed.active_team_id, + auth_method: 'password', + }); + setSessionCookie(res, token); + sendJSON(res, 200, { ok: true, user: { id: landed.user.id, email: landed.user.email, name: landed.user.display_name } }); +} diff --git a/v2/server/auth/session.ts b/v2/server/auth/session.ts new file mode 100644 index 0000000..2548d60 --- /dev/null +++ b/v2/server/auth/session.ts @@ -0,0 +1,60 @@ +// Lifted from V1 agents/social-listening/dashboard/server.ts:75-92. +// V2 session payload is richer: carries user_id + active_team_id (not just username). +import { createHmac, randomBytes } from 'node:crypto'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { envStr, IS_PRODUCTION } from '../lib/env.js'; +import { parseCookies } from '../lib/http.js'; + +export const SESSION_MAX_AGE_SEC = 60 * 60 * 24; // 24h +const SECRET = envStr('SESSION_SECRET') || randomBytes(32).toString('hex'); +export const COOKIE_NAME = 'sl_session_v2'; + +export interface SessionData { + user_id: string; + email: string; + active_team_id: string | null; + auth_method: 'azure-sso' | 'password'; + exp: number; +} + +if (IS_PRODUCTION && !envStr('SESSION_SECRET')) { + throw new Error('SESSION_SECRET must be set in production'); +} + +function sign(payload: string): string { + const sig = createHmac('sha256', SECRET).update(payload).digest('hex'); + return `${payload}.${sig}`; +} + +export function issueSession(data: Omit): string { + const payload = JSON.stringify({ ...data, exp: Date.now() + SESSION_MAX_AGE_SEC * 1000 }); + return sign(payload); +} + +export function getSession(req: IncomingMessage): SessionData | null { + const token = parseCookies(req)[COOKIE_NAME]; + if (!token) return null; + const dot = token.lastIndexOf('.'); + if (dot === -1) return null; + const payload = token.slice(0, dot); + const sig = token.slice(dot + 1); + const expected = createHmac('sha256', SECRET).update(payload).digest('hex'); + if (sig !== expected) return null; + try { + const data = JSON.parse(payload) as SessionData; + if (Date.now() > data.exp) return null; + return data; + } catch { return null; } +} + +export function setSessionCookie(res: ServerResponse, token: string): void { + const secure = IS_PRODUCTION ? '; Secure' : ''; + res.setHeader( + 'Set-Cookie', + `${COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_MAX_AGE_SEC}${secure}`, + ); +} + +export function clearSessionCookie(res: ServerResponse): void { + res.setHeader('Set-Cookie', `${COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0`); +} diff --git a/v2/server/auth/upsert-user.ts b/v2/server/auth/upsert-user.ts new file mode 100644 index 0000000..f3bda77 --- /dev/null +++ b/v2/server/auth/upsert-user.ts @@ -0,0 +1,78 @@ +// V2 SSO persistence: every Azure sign-in upserts a user row and ensures +// the user has at least one team (personal team auto-created on first sign-in). +import { upsertSsoUser, setSuperAdmin, type UserRow } from '../db/users.js'; +import { createTeam, getTeamBySlug } from '../db/teams.js'; +import { addMembership, userHasAnyMembership } from '../db/memberships.js'; +import { envStr } from '../lib/env.js'; + +function emailToSlug(email: string): string { + const local = email.split('@')[0] ?? 'user'; + const base = local.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); + return base || 'user'; +} + +async function uniquePersonalSlug(email: string): Promise { + const base = emailToSlug(email); + let candidate = `${base}-personal`; + let i = 1; + while (await getTeamBySlug(candidate)) { + i += 1; + candidate = `${base}-personal-${i}`; + } + return candidate; +} + +export interface SsoLandingResult { + user: UserRow; + active_team_id: string; + bootstrapped_super_admin: boolean; +} + +/** + * Single entry point for SSO sign-in persistence. + * 1. Upsert user keyed on Azure oid. + * 2. Promote to super-admin if email matches BOOTSTRAP_SUPER_ADMIN_EMAIL. + * 3. If user has no team memberships, create a personal team and add them as owner. + * 4. Pick an active team for the session. + */ +export async function landSsoUser(input: { + oid: string; + email: string; + display_name: string; +}): Promise { + let user = await upsertSsoUser(input); + + // Bootstrap super-admin from env on first sign-in (sticky once true). + let bootstrapped = false; + const bootstrapEmail = envStr('BOOTSTRAP_SUPER_ADMIN_EMAIL').toLowerCase(); + if (bootstrapEmail && !user.is_super_admin && user.email.toLowerCase() === bootstrapEmail) { + user = await setSuperAdmin(user.id, true); + bootstrapped = true; + console.log(`[auth] Bootstrapped super-admin: ${user.email}`); + } + + // Ensure at least one team membership. + const hasTeam = await userHasAnyMembership(user.id); + let activeTeamId: string; + if (!hasTeam) { + const slug = await uniquePersonalSlug(user.email); + const personalTeam = await createTeam({ + slug, + name: `${user.display_name || user.email}'s workspace`, + isPersonal: true, + }); + await addMembership({ teamId: personalTeam.id, userId: user.id, role: 'owner', addedBy: null }); + activeTeamId = personalTeam.id; + } else { + // Pick the user's first team as the default active team. + const { sql } = await import('../db/client.js'); + const [row] = await sql<{ team_id: string }[]>` + SELECT team_id FROM team_memberships WHERE user_id = ${user.id} + ORDER BY added_at ASC LIMIT 1 + `; + if (!row) throw new Error('landSsoUser: user has membership but no row returned'); + activeTeamId = row.team_id; + } + + return { user, active_team_id: activeTeamId, bootstrapped_super_admin: bootstrapped }; +} diff --git a/v2/server/db/briefs.ts b/v2/server/db/briefs.ts new file mode 100644 index 0000000..d693cb4 --- /dev/null +++ b/v2/server/db/briefs.ts @@ -0,0 +1,69 @@ +import { sql } from './client.js'; +import type { BriefInput } from '../schemas/brief.js'; + +export interface BriefRow { + id: string; + team_id: string; + owner_id: string; + slug: string; + client_name: string; + category: string; + business_question: string; + date_window_days: number; + budget_usd: number; + platforms: string[]; + positioning: unknown; + kpis: unknown; + context_vision: string | null; + min_likes: number; + min_plays: number; + min_stl_pct: number; + prior_report_id: string | null; + brief_yaml: unknown; + created_at: Date; +} + +export async function createBrief(input: { + team_id: string; + owner_id: string; + slug: string; + brief: BriefInput; +}): Promise { + const b = input.brief; + const [row] = await sql` + INSERT INTO briefs ( + team_id, owner_id, slug, client_name, category, business_question, + date_window_days, budget_usd, platforms, + positioning, kpis, context_vision, + min_likes, min_plays, min_stl_pct, + prior_report_id, brief_yaml + ) VALUES ( + ${input.team_id}, ${input.owner_id}, ${input.slug}, + ${b.client_name}, ${b.category}, ${b.business_question}, + ${b.date_window_days}, ${b.budget_usd}, ${b.platforms}::text[], + ${b.brand.positioning ? sql.json({ positioning: b.brand.positioning, brand: b.brand }) : null}, + ${sql.json(b.kpis)}, ${b.context_vision ?? null}, + ${b.min_likes}, ${b.min_plays}, ${b.min_stl_pct}, + ${b.prior_report_id ?? null}, ${sql.json(b)} + ) + RETURNING * + `; + if (!row) throw new Error('createBrief: no row returned'); + return row; +} + +export async function listBriefsForTeam(teamId: string): Promise { + return sql` + SELECT * FROM briefs WHERE team_id = ${teamId} ORDER BY created_at DESC + `; +} + +export async function getBriefById(id: string): Promise { + const [row] = await sql`SELECT * FROM briefs WHERE id = ${id}`; + return row ?? null; +} + +export async function deleteBrief(id: string): Promise { + const result = await sql`DELETE FROM briefs WHERE id = ${id}`; + return result.count > 0; +} diff --git a/v2/server/db/client.ts b/v2/server/db/client.ts new file mode 100644 index 0000000..f229280 --- /dev/null +++ b/v2/server/db/client.ts @@ -0,0 +1,13 @@ +import postgres from 'postgres'; +import { envStr } from '../lib/env.js'; + +const DATABASE_URL = envStr('DATABASE_URL') || + 'postgresql://srv2_user:change-me-please@localhost:5437/social_reporting_v2'; + +export const sql = postgres(DATABASE_URL, { + max: 10, + idle_timeout: 30, + connect_timeout: 10, +}); + +export type Sql = typeof sql; diff --git a/v2/server/db/memberships.ts b/v2/server/db/memberships.ts new file mode 100644 index 0000000..5eda798 --- /dev/null +++ b/v2/server/db/memberships.ts @@ -0,0 +1,81 @@ +import { sql } from './client.js'; + +export type TeamRole = 'owner' | 'admin' | 'editor' | 'viewer'; + +export interface MembershipRow { + team_id: string; + user_id: string; + role: TeamRole; + added_by: string | null; + added_at: Date; +} + +export interface MembershipWithUser extends MembershipRow { + email: string; + display_name: string; +} + +export async function getMembership(teamId: string, userId: string): Promise { + const [row] = await sql` + SELECT * FROM team_memberships WHERE team_id = ${teamId} AND user_id = ${userId} + `; + return row ?? null; +} + +export async function addMembership(input: { + teamId: string; + userId: string; + role: TeamRole; + addedBy: string | null; +}): Promise { + const [row] = await sql` + INSERT INTO team_memberships (team_id, user_id, role, added_by) + VALUES (${input.teamId}, ${input.userId}, ${input.role}, ${input.addedBy}) + RETURNING * + `; + if (!row) throw new Error('addMembership: no row returned'); + return row; +} + +export async function updateMembershipRole( + teamId: string, + userId: string, + role: TeamRole, +): Promise { + const [row] = await sql` + UPDATE team_memberships SET role = ${role} + WHERE team_id = ${teamId} AND user_id = ${userId} + RETURNING * + `; + return row ?? null; +} + +export async function removeMembership(teamId: string, userId: string): Promise { + const result = await sql` + DELETE FROM team_memberships WHERE team_id = ${teamId} AND user_id = ${userId} + `; + return result.count > 0; +} + +export async function listMembers(teamId: string): Promise { + return sql` + SELECT m.*, u.email, u.display_name + FROM team_memberships m + JOIN users u ON u.id = m.user_id + WHERE m.team_id = ${teamId} + ORDER BY m.added_at ASC + `; +} + +export async function userHasAnyMembership(userId: string): Promise { + const [row] = await sql<{ exists: boolean }[]>` + SELECT EXISTS(SELECT 1 FROM team_memberships WHERE user_id = ${userId}) AS exists + `; + return row?.exists ?? false; +} + +const ROLE_RANK: Record = { viewer: 0, editor: 1, admin: 2, owner: 3 }; + +export function roleAtLeast(actual: TeamRole, required: TeamRole): boolean { + return ROLE_RANK[actual] >= ROLE_RANK[required]; +} diff --git a/v2/server/db/teams.ts b/v2/server/db/teams.ts new file mode 100644 index 0000000..d4ea567 --- /dev/null +++ b/v2/server/db/teams.ts @@ -0,0 +1,44 @@ +import { sql } from './client.js'; +import type { TeamRole } from './memberships.js'; + +export interface TeamRow { + id: string; + slug: string; + name: string; + is_personal: boolean; + created_at: Date; +} + +export async function getTeamById(id: string): Promise { + const [row] = await sql`SELECT * FROM teams WHERE id = ${id}`; + return row ?? null; +} + +export async function getTeamBySlug(slug: string): Promise { + const [row] = await sql`SELECT * FROM teams WHERE slug = ${slug}`; + return row ?? null; +} + +export async function createTeam(input: { slug: string; name: string; isPersonal: boolean }): Promise { + const [row] = await sql` + INSERT INTO teams (slug, name, is_personal) + VALUES (${input.slug}, ${input.name}, ${input.isPersonal}) + RETURNING * + `; + if (!row) throw new Error('createTeam: no row returned'); + return row; +} + +export interface TeamWithRole extends TeamRow { + role: TeamRole; +} + +export async function listTeamsForUser(userId: string): Promise { + return sql` + SELECT t.*, m.role + FROM teams t + JOIN team_memberships m ON m.team_id = t.id + WHERE m.user_id = ${userId} + ORDER BY t.is_personal DESC, t.created_at ASC + `; +} diff --git a/v2/server/db/users.ts b/v2/server/db/users.ts new file mode 100644 index 0000000..f75fc11 --- /dev/null +++ b/v2/server/db/users.ts @@ -0,0 +1,59 @@ +import { sql } from './client.js'; + +export interface UserRow { + id: string; + azure_oid: string; + email: string; + display_name: string; + is_super_admin: boolean; + password_hash: string | null; + created_at: Date; + last_login_at: Date | null; +} + +export async function getUserById(id: string): Promise { + const [row] = await sql`SELECT * FROM users WHERE id = ${id}`; + return row ?? null; +} + +export async function getUserByOid(oid: string): Promise { + const [row] = await sql`SELECT * FROM users WHERE azure_oid = ${oid}`; + return row ?? null; +} + +export async function getUserByEmail(email: string): Promise { + const [row] = await sql`SELECT * FROM users WHERE email = ${email}`; + return row ?? null; +} + +export interface UpsertSsoUserInput { + oid: string; + email: string; + display_name: string; +} + +export async function upsertSsoUser(input: UpsertSsoUserInput): Promise { + const [row] = await sql` + INSERT INTO users (azure_oid, email, display_name, last_login_at) + VALUES (${input.oid}, ${input.email}, ${input.display_name}, NOW()) + ON CONFLICT (azure_oid) DO UPDATE SET + email = EXCLUDED.email, + display_name = EXCLUDED.display_name, + last_login_at = NOW() + RETURNING * + `; + if (!row) throw new Error('upsertSsoUser: no row returned'); + return row; +} + +export async function setSuperAdmin(userId: string, isSuper: boolean): Promise { + const [row] = await sql` + UPDATE users SET is_super_admin = ${isSuper} WHERE id = ${userId} RETURNING * + `; + if (!row) throw new Error('setSuperAdmin: user not found'); + return row; +} + +export async function listAllUsers(): Promise { + return sql`SELECT * FROM users ORDER BY created_at DESC`; +} diff --git a/v2/server/index.ts b/v2/server/index.ts new file mode 100644 index 0000000..6065558 --- /dev/null +++ b/v2/server/index.ts @@ -0,0 +1,175 @@ +#!/usr/bin/env tsx +// V2 HTTP server: serves the operator-app SPA + JSON API. +// Routing is plain http + URL pattern matching (no Express). +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { join, resolve, normalize, extname } from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { envInt, envStr, IS_PRODUCTION } from './lib/env.js'; +import { assertComposeNameOrExit } from './lib/compose-name-guard.js'; +import { sendJSON, sendText } from './lib/http.js'; + +import { handleSsoTokenExchange, handleLogout } from './routes/sso.js'; +import { handlePasswordLogin } from './auth/password-fallback.js'; +import { handleGetMe, handlePatchActiveTeam, handleGetSession } from './routes/me.js'; +import { + handleListTeams, handleCreateTeam, handleGetTeam, + handleAddMember, handleUpdateMemberRole, handleRemoveMember, +} from './routes/teams.js'; +import { handleListAllUsers, handleToggleSuperAdmin } from './routes/admin.js'; +import { + handleListBriefs, handleCreateBrief, handleGetBrief, handleDeleteBrief, +} from './routes/briefs.js'; + +assertComposeNameOrExit(); + +const PORT = envInt('PORT', 3457); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const SPA_DIST = resolve(__dirname, '../operator-app/dist'); +const ALLOWED_ORIGIN = envStr('ALLOWED_ORIGIN'); + +const MIME: Record = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.ico': 'image/x-icon', + '.woff2':'font/woff2', + '.woff': 'font/woff', +}; + +function setSecurityHeaders(res: ServerResponse): void { + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Referrer-Policy', 'no-referrer'); + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + + "font-src 'self' https://fonts.gstatic.com; " + + "img-src 'self' data: blob: https:; " + + "connect-src 'self' https://login.microsoftonline.com; " + + "frame-src 'self' https://login.microsoftonline.com", + ); +} + +function applyCors(req: IncomingMessage, res: ServerResponse): void { + const origin = req.headers.origin || ''; + if (ALLOWED_ORIGIN === '*') { + res.setHeader('Access-Control-Allow-Origin', '*'); + } else if (ALLOWED_ORIGIN && origin === ALLOWED_ORIGIN) { + res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGIN); + res.setHeader('Vary', 'Origin'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); +} + +function serveStatic(req: IncomingMessage, res: ServerResponse, urlPath: string): boolean { + if (!existsSync(SPA_DIST)) return false; + + let p = urlPath === '/' ? '/index.html' : urlPath; + // Defence in depth — the URL parser already strips `..`, but normalize anyway. + p = normalize(p).replace(/^(\.\.[/\\])+/, ''); + const candidate = join(SPA_DIST, p); + if (!candidate.startsWith(SPA_DIST)) return false; + + if (existsSync(candidate) && statSync(candidate).isFile()) { + const ext = extname(candidate).toLowerCase(); + const mime = MIME[ext] || 'application/octet-stream'; + res.setHeader('Cache-Control', ext === '.html' ? 'no-cache' : 'public, max-age=3600'); + res.writeHead(200, { 'Content-Type': mime }); + res.end(readFileSync(candidate)); + return true; + } + return false; +} + +function spaFallback(res: ServerResponse): void { + const indexFile = join(SPA_DIST, 'index.html'); + if (existsSync(indexFile)) { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' }); + res.end(readFileSync(indexFile)); + } else { + sendText(res, 503, 'Operator-app not built yet. Run: npm run ui:build'); + } +} + +interface Route { + method: string; + pattern: RegExp; + handle: (req: IncomingMessage, res: ServerResponse, params: string[]) => Promise | void; +} + +const ROUTES: Route[] = [ + { method: 'GET', pattern: /^\/api\/health$/, handle: (_req, res) => sendJSON(res, 200, { ok: true }) }, + { method: 'GET', pattern: /^\/api\/auth$/, handle: handleGetSession }, + { method: 'POST', pattern: /^\/api\/sso\/token-exchange$/, handle: handleSsoTokenExchange }, + { method: 'POST', pattern: /^\/api\/login$/, handle: handlePasswordLogin }, + { method: 'GET', pattern: /^\/api\/logout$/, handle: handleLogout }, + { method: 'GET', pattern: /^\/api\/me$/, handle: handleGetMe }, + { method: 'PATCH', pattern: /^\/api\/me\/active-team$/, handle: handlePatchActiveTeam }, + { method: 'GET', pattern: /^\/api\/teams$/, handle: handleListTeams }, + { method: 'POST', pattern: /^\/api\/teams$/, handle: handleCreateTeam }, + { method: 'GET', pattern: /^\/api\/teams\/([0-9a-f-]{36})$/, handle: (req, res, [id]) => handleGetTeam(req, res, id!) }, + { method: 'POST', pattern: /^\/api\/teams\/([0-9a-f-]{36})\/members$/, handle: (req, res, [id]) => handleAddMember(req, res, id!) }, + { method: 'PATCH', pattern: /^\/api\/teams\/([0-9a-f-]{36})\/members\/([0-9a-f-]{36})\/role$/, handle: (req, res, [t, u]) => handleUpdateMemberRole(req, res, t!, u!) }, + { method: 'DELETE', pattern: /^\/api\/teams\/([0-9a-f-]{36})\/members\/([0-9a-f-]{36})$/, handle: (req, res, [t, u]) => handleRemoveMember(req, res, t!, u!) }, + { method: 'GET', pattern: /^\/api\/admin\/users$/, handle: handleListAllUsers }, + { method: 'PATCH', pattern: /^\/api\/admin\/users\/([0-9a-f-]{36})\/super$/, handle: (req, res, [id]) => handleToggleSuperAdmin(req, res, id!) }, + { method: 'GET', pattern: /^\/api\/briefs$/, handle: handleListBriefs }, + { method: 'POST', pattern: /^\/api\/briefs$/, handle: handleCreateBrief }, + { method: 'GET', pattern: /^\/api\/briefs\/([0-9a-f-]{36})$/, handle: (req, res, [id]) => handleGetBrief(req, res, id!) }, + { method: 'DELETE', pattern: /^\/api\/briefs\/([0-9a-f-]{36})$/, handle: (req, res, [id]) => handleDeleteBrief(req, res, id!) }, +]; + +async function route(req: IncomingMessage, res: ServerResponse): Promise { + const url = new URL(req.url || '/', `http://localhost:${PORT}`); + const pathname = url.pathname; + const method = req.method || 'GET'; + + for (const r of ROUTES) { + if (r.method !== method) continue; + const m = r.pattern.exec(pathname); + if (!m) continue; + await r.handle(req, res, m.slice(1)); + return; + } + + if (pathname.startsWith('/api/')) { + sendJSON(res, 404, { error: 'Not found' }); + return; + } + + if (method === 'GET') { + if (serveStatic(req, res, pathname)) return; + spaFallback(res); + return; + } + sendJSON(res, 404, { error: 'Not found' }); +} + +const server = createServer(async (req, res) => { + setSecurityHeaders(res); + applyCors(req, res); + if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } + try { + await route(req, res); + } catch (err) { + console.error('[server] unhandled error:', err); + if (!res.headersSent) sendJSON(res, 500, { error: 'Internal server error' }); + } +}); + +server.listen(PORT, () => { + console.log(`[v2] listening on :${PORT} (${IS_PRODUCTION ? 'prod' : 'dev'})`); +}); diff --git a/v2/server/lib/compose-name-guard.ts b/v2/server/lib/compose-name-guard.ts new file mode 100644 index 0000000..00e1613 --- /dev/null +++ b/v2/server/lib/compose-name-guard.ts @@ -0,0 +1,26 @@ +import { envStr, IS_PRODUCTION } from './env.js'; + +const EXPECTED = 'social-reporting-v2'; + +/** + * V2 ships as a separate Docker Compose project that MUST share neither containers nor + * volumes with V1. Compose derives the project name from the parent directory by default, + * which on the shared optical-dev server collapses everything under `deploy/` onto one + * project name and lets apps silently evict each other's data. Per CLAUDE.md, every + * compose file must pin a unique `name:` field. This boot guard catches the case where + * the env var didn't make it through. + * + * In dev we warn; in prod we refuse to start. + */ +export function assertComposeNameOrExit(): void { + const actual = envStr('COMPOSE_PROJECT_NAME'); + if (actual === EXPECTED) return; + + const msg = `[compose-name-guard] COMPOSE_PROJECT_NAME='${actual || '(unset)'}' — expected '${EXPECTED}'. ` + + `If you're running outside Docker this is fine; otherwise check docker-compose.v2.yml.`; + if (IS_PRODUCTION) { + console.error(msg); + process.exit(1); + } + console.warn(msg); +} diff --git a/v2/server/lib/env.ts b/v2/server/lib/env.ts new file mode 100644 index 0000000..950d4e4 --- /dev/null +++ b/v2/server/lib/env.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function loadDotenv(): Record { + const env: Record = {}; + const candidates = [ + resolve(__dirname, '../../.env'), + resolve(__dirname, '../../../.env'), + resolve(process.cwd(), '.env'), + ]; + for (const p of candidates) { + try { + const txt = readFileSync(p, 'utf-8'); + for (const raw of txt.split('\n')) { + const t = raw.trim(); + if (!t || t.startsWith('#')) continue; + const eq = t.indexOf('='); + if (eq === -1) continue; + env[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^["']|["']$/g, ''); + } + break; + } catch { /* try next */ } + } + return env; +} + +const fileEnv = loadDotenv(); + +export function envStr(key: string, fallback = ''): string { + return process.env[key] ?? fileEnv[key] ?? fallback; +} + +export function envInt(key: string, fallback: number): number { + const v = envStr(key); + if (!v) return fallback; + const n = parseInt(v, 10); + return Number.isFinite(n) ? n : fallback; +} + +export function envBool(key: string, fallback = false): boolean { + const v = envStr(key).toLowerCase(); + if (!v) return fallback; + return v === 'true' || v === '1' || v === 'yes'; +} + +export const IS_PRODUCTION = envStr('NODE_ENV') === 'production'; diff --git a/v2/server/lib/http.ts b/v2/server/lib/http.ts new file mode 100644 index 0000000..341ca96 --- /dev/null +++ b/v2/server/lib/http.ts @@ -0,0 +1,52 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; + +export const MAX_BODY_SIZE = 1024 * 1024; // 1 MB + +export function sendJSON(res: ServerResponse, status: number, data: unknown): void { + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); +} + +export function sendText(res: ServerResponse, status: number, text: string, contentType = 'text/plain'): void { + res.writeHead(status, { 'Content-Type': contentType }); + res.end(text); +} + +export function parseBody(req: IncomingMessage): Promise { + return new Promise((resolveP, reject) => { + const chunks: Buffer[] = []; + let size = 0; + req.on('data', (c: Buffer) => { + size += c.length; + if (size > MAX_BODY_SIZE) { + req.destroy(); + reject(new Error('Request body too large')); + return; + } + chunks.push(c); + }); + req.on('end', () => resolveP(Buffer.concat(chunks).toString())); + req.on('error', reject); + }); +} + +export async function parseJSONBody(req: IncomingMessage): Promise { + const body = await parseBody(req); + return JSON.parse(body) as T; +} + +export function parseCookies(req: IncomingMessage): Record { + const cookies: Record = {}; + const header = req.headers.cookie || ''; + for (const pair of header.split(';')) { + const eq = pair.indexOf('='); + if (eq === -1) continue; + cookies[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim(); + } + return cookies; +} + +export function clientIp(req: IncomingMessage): string { + const xff = (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim(); + return xff || req.socket.remoteAddress || 'unknown'; +} diff --git a/v2/server/middleware/require-auth.ts b/v2/server/middleware/require-auth.ts new file mode 100644 index 0000000..f72dbd8 --- /dev/null +++ b/v2/server/middleware/require-auth.ts @@ -0,0 +1,17 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { getSession, type SessionData } from '../auth/session.js'; +import { sendJSON } from '../lib/http.js'; + +export interface AuthedReq extends IncomingMessage { + session: SessionData; +} + +export function requireAuth(req: IncomingMessage, res: ServerResponse): SessionData | null { + const session = getSession(req); + if (!session) { + sendJSON(res, 401, { error: 'Not authenticated' }); + return null; + } + (req as AuthedReq).session = session; + return session; +} diff --git a/v2/server/middleware/require-super-admin.ts b/v2/server/middleware/require-super-admin.ts new file mode 100644 index 0000000..6ab1330 --- /dev/null +++ b/v2/server/middleware/require-super-admin.ts @@ -0,0 +1,20 @@ +import type { ServerResponse } from 'node:http'; +import { getUserById } from '../db/users.js'; +import { sendJSON } from '../lib/http.js'; +import type { SessionData } from '../auth/session.js'; + +export async function requireSuperAdmin( + res: ServerResponse, + session: SessionData, +): Promise { + const user = await getUserById(session.user_id); + if (!user) { + sendJSON(res, 401, { error: 'User not found' }); + return false; + } + if (!user.is_super_admin) { + sendJSON(res, 403, { error: 'Super-admin required' }); + return false; + } + return true; +} diff --git a/v2/server/middleware/require-team-role.ts b/v2/server/middleware/require-team-role.ts new file mode 100644 index 0000000..da3620f --- /dev/null +++ b/v2/server/middleware/require-team-role.ts @@ -0,0 +1,32 @@ +// Single source of truth for resource-level authorisation. +// Order: super-admin bypass → membership lookup → role check. +import type { ServerResponse } from 'node:http'; +import { getMembership, roleAtLeast, type TeamRole } from '../db/memberships.js'; +import { getUserById } from '../db/users.js'; +import { sendJSON } from '../lib/http.js'; +import type { SessionData } from '../auth/session.js'; + +export async function requireTeamRole( + res: ServerResponse, + session: SessionData, + teamId: string, + required: TeamRole, +): Promise { + const user = await getUserById(session.user_id); + if (!user) { + sendJSON(res, 401, { error: 'User not found' }); + return false; + } + if (user.is_super_admin) return true; + + const m = await getMembership(teamId, user.id); + if (!m) { + sendJSON(res, 403, { error: 'Not a member of this team' }); + return false; + } + if (!roleAtLeast(m.role, required)) { + sendJSON(res, 403, { error: `Requires role >= ${required}` }); + return false; + } + return true; +} diff --git a/v2/server/routes/admin.ts b/v2/server/routes/admin.ts new file mode 100644 index 0000000..9e51956 --- /dev/null +++ b/v2/server/routes/admin.ts @@ -0,0 +1,47 @@ +// Super-admin only endpoints. +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { requireAuth } from '../middleware/require-auth.js'; +import { requireSuperAdmin } from '../middleware/require-super-admin.js'; +import { listAllUsers, setSuperAdmin, getUserById } from '../db/users.js'; +import { parseJSONBody, sendJSON } from '../lib/http.js'; + +export async function handleListAllUsers(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireSuperAdmin(res, session)) return; + const users = await listAllUsers(); + sendJSON(res, 200, { + users: users.map((u) => ({ + id: u.id, email: u.email, display_name: u.display_name, + is_super_admin: u.is_super_admin, + created_at: u.created_at, last_login_at: u.last_login_at, + })), + }); +} + +export async function handleToggleSuperAdmin( + req: IncomingMessage, res: ServerResponse, userId: string, +): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireSuperAdmin(res, session)) return; + + let is_super_admin: boolean | undefined; + try { + const body = await parseJSONBody<{ is_super_admin?: boolean }>(req); + is_super_admin = body.is_super_admin; + } catch { sendJSON(res, 400, { error: 'Invalid body' }); return; } + if (typeof is_super_admin !== 'boolean') { + sendJSON(res, 400, { error: 'is_super_admin must be a boolean' }); + return; + } + // Prevent self-demotion (avoid locking everyone out). + if (session.user_id === userId && !is_super_admin) { + sendJSON(res, 400, { error: 'Cannot remove your own super-admin role' }); + return; + } + const target = await getUserById(userId); + if (!target) { sendJSON(res, 404, { error: 'User not found' }); return; } + const updated = await setSuperAdmin(userId, is_super_admin); + sendJSON(res, 200, { ok: true, user: { id: updated.id, email: updated.email, is_super_admin: updated.is_super_admin } }); +} diff --git a/v2/server/routes/briefs.ts b/v2/server/routes/briefs.ts new file mode 100644 index 0000000..e12084d --- /dev/null +++ b/v2/server/routes/briefs.ts @@ -0,0 +1,98 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { requireAuth } from '../middleware/require-auth.js'; +import { requireTeamRole } from '../middleware/require-team-role.js'; +import { BRIEF_INPUT, defaultBriefSlug } from '../schemas/brief.js'; +import { + createBrief, deleteBrief, getBriefById, listBriefsForTeam, type BriefRow, +} from '../db/briefs.js'; +import { parseJSONBody, sendJSON } from '../lib/http.js'; + +function publicBrief(b: BriefRow): unknown { + // brief_yaml stores the full validated brief shape — surface it so the operator UI + // can render every field (competitors, audience, geo, language, brand handle, etc.) + // The dedicated columns (min_likes/min_plays/etc.) are also exposed for query convenience. + return { + id: b.id, + team_id: b.team_id, + owner_id: b.owner_id, + slug: b.slug, + client_name: b.client_name, + category: b.category, + business_question: b.business_question, + date_window_days: b.date_window_days, + budget_usd: Number(b.budget_usd), + platforms: b.platforms, + kpis: b.kpis, + context_vision: b.context_vision, + min_likes: b.min_likes, + min_plays: b.min_plays, + min_stl_pct: Number(b.min_stl_pct), + prior_report_id: b.prior_report_id, + created_at: b.created_at, + full: b.brief_yaml, + }; +} + +export async function handleListBriefs(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!session.active_team_id) { sendJSON(res, 200, { briefs: [] }); return; } + if (!await requireTeamRole(res, session, session.active_team_id, 'viewer')) return; + const briefs = await listBriefsForTeam(session.active_team_id); + sendJSON(res, 200, { briefs: briefs.map(publicBrief) }); +} + +export async function handleCreateBrief(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!session.active_team_id) { sendJSON(res, 400, { error: 'No active team' }); return; } + if (!await requireTeamRole(res, session, session.active_team_id, 'editor')) return; + + let raw: unknown; + try { raw = await parseJSONBody(req); } + catch { sendJSON(res, 400, { error: 'Invalid JSON body' }); return; } + + const parsed = BRIEF_INPUT.safeParse(raw); + if (!parsed.success) { + sendJSON(res, 400, { error: 'Validation failed', issues: parsed.error.issues }); + return; + } + const brief = parsed.data; + const slug = brief.slug || defaultBriefSlug(brief.client_name); + + try { + const row = await createBrief({ + team_id: session.active_team_id, + owner_id: session.user_id, + slug, + brief, + }); + sendJSON(res, 201, { brief: publicBrief(row) }); + } catch (err) { + if ((err as { code?: string }).code === '23505') { + sendJSON(res, 409, { error: `A brief with slug '${slug}' already exists in this team` }); + } else { + console.error('[briefs] create error:', err); + sendJSON(res, 500, { error: (err as Error).message }); + } + } +} + +export async function handleGetBrief(req: IncomingMessage, res: ServerResponse, id: string): Promise { + const session = requireAuth(req, res); + if (!session) return; + const brief = await getBriefById(id); + if (!brief) { sendJSON(res, 404, { error: 'Brief not found' }); return; } + if (!await requireTeamRole(res, session, brief.team_id, 'viewer')) return; + sendJSON(res, 200, { brief: publicBrief(brief) }); +} + +export async function handleDeleteBrief(req: IncomingMessage, res: ServerResponse, id: string): Promise { + const session = requireAuth(req, res); + if (!session) return; + const brief = await getBriefById(id); + if (!brief) { sendJSON(res, 404, { error: 'Brief not found' }); return; } + if (!await requireTeamRole(res, session, brief.team_id, 'admin')) return; + await deleteBrief(id); + sendJSON(res, 200, { ok: true }); +} diff --git a/v2/server/routes/me.ts b/v2/server/routes/me.ts new file mode 100644 index 0000000..610636b --- /dev/null +++ b/v2/server/routes/me.ts @@ -0,0 +1,63 @@ +// /api/me — user profile + memberships + active team. +// PATCH /api/me/active-team — switch active team (must be a team the user belongs to). +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { requireAuth } from '../middleware/require-auth.js'; +import { getUserById } from '../db/users.js'; +import { listTeamsForUser } from '../db/teams.js'; +import { issueSession, setSessionCookie, getSession } from '../auth/session.js'; +import { parseJSONBody, sendJSON } from '../lib/http.js'; + +export async function handleGetMe(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + const user = await getUserById(session.user_id); + if (!user) { sendJSON(res, 401, { error: 'User not found' }); return; } + const teams = await listTeamsForUser(user.id); + const activeTeam = teams.find((t) => t.id === session.active_team_id) ?? teams[0] ?? null; + sendJSON(res, 200, { + user: { + id: user.id, + email: user.email, + display_name: user.display_name, + is_super_admin: user.is_super_admin, + auth_method: session.auth_method, + }, + teams: teams.map((t) => ({ + id: t.id, slug: t.slug, name: t.name, is_personal: t.is_personal, role: t.role, + })), + active_team: activeTeam ? { + id: activeTeam.id, slug: activeTeam.slug, name: activeTeam.name, + is_personal: activeTeam.is_personal, role: activeTeam.role, + } : null, + }); +} + +export async function handlePatchActiveTeam(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + let team_id: string | undefined; + try { + const body = await parseJSONBody<{ team_id?: string }>(req); + team_id = body.team_id; + } catch { sendJSON(res, 400, { error: 'Invalid body' }); return; } + if (!team_id) { sendJSON(res, 400, { error: 'team_id required' }); return; } + + const teams = await listTeamsForUser(session.user_id); + const target = teams.find((t) => t.id === team_id); + if (!target) { sendJSON(res, 403, { error: 'Not a member of that team' }); return; } + + const newToken = issueSession({ + user_id: session.user_id, + email: session.email, + active_team_id: target.id, + auth_method: session.auth_method, + }); + setSessionCookie(res, newToken); + sendJSON(res, 200, { ok: true, active_team_id: target.id }); +} + +export function handleGetSession(req: IncomingMessage, res: ServerResponse): void { + const session = getSession(req); + if (!session) { sendJSON(res, 401, { ok: false }); return; } + sendJSON(res, 200, { ok: true, user_id: session.user_id, email: session.email, active_team_id: session.active_team_id }); +} diff --git a/v2/server/routes/sso.ts b/v2/server/routes/sso.ts new file mode 100644 index 0000000..8a34907 --- /dev/null +++ b/v2/server/routes/sso.ts @@ -0,0 +1,66 @@ +// /api/sso/token-exchange — verify Azure ID token, upsert user, ensure team, issue session. +// Adapted from V1 dashboard/server.ts:388-426 with V2's persistence layer added. +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { verifyAzureIdToken, SSO_ENABLED } from '../auth/jwks.js'; +import { landSsoUser } from '../auth/upsert-user.js'; +import { issueSession, setSessionCookie, clearSessionCookie } from '../auth/session.js'; +import { parseJSONBody, sendJSON } from '../lib/http.js'; + +export async function handleSsoTokenExchange(req: IncomingMessage, res: ServerResponse): Promise { + if (!SSO_ENABLED) { + sendJSON(res, 404, { ok: false, error: 'SSO not configured' }); + return; + } + let idToken: string | undefined; + try { + const body = await parseJSONBody<{ idToken?: string }>(req); + idToken = body.idToken; + } catch { + sendJSON(res, 400, { ok: false, error: 'Invalid request body' }); + return; + } + if (!idToken) { + sendJSON(res, 400, { ok: false, error: 'Missing idToken' }); + return; + } + + const result = await verifyAzureIdToken(idToken); + if (!result.valid || !result.claims) { + console.log(`[sso] token validation failed: ${result.error}`); + sendJSON(res, 401, { ok: false, error: result.error ?? 'Invalid token' }); + return; + } + const claims = result.claims; + const email = (claims.email || claims.preferred_username || '') as string; + const display_name = (claims.name || email || 'Unnamed user'); + if (!email) { + sendJSON(res, 401, { ok: false, error: 'Token missing email/preferred_username' }); + return; + } + + try { + const landed = await landSsoUser({ oid: claims.oid, email, display_name }); + const token = issueSession({ + user_id: landed.user.id, + email: landed.user.email, + active_team_id: landed.active_team_id, + auth_method: 'azure-sso', + }); + setSessionCookie(res, token); + sendJSON(res, 200, { + ok: true, + user: { id: landed.user.id, email: landed.user.email, name: landed.user.display_name, is_super_admin: landed.user.is_super_admin }, + active_team_id: landed.active_team_id, + bootstrapped_super_admin: landed.bootstrapped_super_admin, + }); + console.log(`[sso] sign-in: ${email}${landed.bootstrapped_super_admin ? ' (bootstrapped super-admin)' : ''}`); + } catch (err) { + console.error('[sso] landing error:', (err as Error).message); + sendJSON(res, 500, { ok: false, error: 'Sign-in failed' }); + } +} + +export function handleLogout(_req: IncomingMessage, res: ServerResponse): void { + clearSessionCookie(res); + sendJSON(res, 200, { ok: true }); +} diff --git a/v2/server/routes/teams.ts b/v2/server/routes/teams.ts new file mode 100644 index 0000000..283867b --- /dev/null +++ b/v2/server/routes/teams.ts @@ -0,0 +1,116 @@ +// Team CRUD + member management. +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { requireAuth } from '../middleware/require-auth.js'; +import { requireTeamRole } from '../middleware/require-team-role.js'; +import { createTeam, getTeamById, getTeamBySlug, listTeamsForUser } from '../db/teams.js'; +import { + addMembership, listMembers, removeMembership, updateMembershipRole, + type TeamRole, +} from '../db/memberships.js'; +import { getUserByEmail } from '../db/users.js'; +import { parseJSONBody, sendJSON } from '../lib/http.js'; + +const VALID_ROLES: TeamRole[] = ['owner', 'admin', 'editor', 'viewer']; + +function slugify(s: string): string { + return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 60) || 'team'; +} + +export async function handleListTeams(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + const teams = await listTeamsForUser(session.user_id); + sendJSON(res, 200, { teams }); +} + +export async function handleCreateTeam(req: IncomingMessage, res: ServerResponse): Promise { + const session = requireAuth(req, res); + if (!session) return; + let name: string | undefined; + try { + const body = await parseJSONBody<{ name?: string }>(req); + name = body.name?.trim(); + } catch { sendJSON(res, 400, { error: 'Invalid body' }); return; } + if (!name || name.length < 2) { sendJSON(res, 400, { error: 'name required (min 2 chars)' }); return; } + + let slug = slugify(name); + let suffix = 1; + while (await getTeamBySlug(slug)) { + suffix += 1; + slug = `${slugify(name)}-${suffix}`; + } + const team = await createTeam({ slug, name, isPersonal: false }); + await addMembership({ teamId: team.id, userId: session.user_id, role: 'owner', addedBy: session.user_id }); + sendJSON(res, 201, { team: { ...team, role: 'owner' } }); +} + +export async function handleGetTeam(req: IncomingMessage, res: ServerResponse, teamId: string): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireTeamRole(res, session, teamId, 'viewer')) return; + const team = await getTeamById(teamId); + if (!team) { sendJSON(res, 404, { error: 'Team not found' }); return; } + const members = await listMembers(teamId); + sendJSON(res, 200, { team, members }); +} + +export async function handleAddMember(req: IncomingMessage, res: ServerResponse, teamId: string): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireTeamRole(res, session, teamId, 'admin')) return; + let email: string | undefined, role: string | undefined; + try { + const body = await parseJSONBody<{ email?: string; role?: string }>(req); + email = body.email?.trim(); + role = body.role; + } catch { sendJSON(res, 400, { error: 'Invalid body' }); return; } + if (!email || !role) { sendJSON(res, 400, { error: 'email and role required' }); return; } + if (!VALID_ROLES.includes(role as TeamRole)) { sendJSON(res, 400, { error: 'Invalid role' }); return; } + + const target = await getUserByEmail(email); + if (!target) { + sendJSON(res, 404, { error: 'User not found. They must sign in via SSO at least once before being invited.' }); + return; + } + try { + await addMembership({ teamId, userId: target.id, role: role as TeamRole, addedBy: session.user_id }); + sendJSON(res, 201, { ok: true, user_id: target.id }); + } catch (err) { + if ((err as { code?: string }).code === '23505') { + sendJSON(res, 409, { error: 'User is already a member' }); + } else { + sendJSON(res, 500, { error: (err as Error).message }); + } + } +} + +export async function handleUpdateMemberRole( + req: IncomingMessage, res: ServerResponse, teamId: string, userId: string, +): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireTeamRole(res, session, teamId, 'owner')) return; + let role: string | undefined; + try { + const body = await parseJSONBody<{ role?: string }>(req); + role = body.role; + } catch { sendJSON(res, 400, { error: 'Invalid body' }); return; } + if (!role || !VALID_ROLES.includes(role as TeamRole)) { + sendJSON(res, 400, { error: 'Invalid role' }); + return; + } + const updated = await updateMembershipRole(teamId, userId, role as TeamRole); + if (!updated) { sendJSON(res, 404, { error: 'Membership not found' }); return; } + sendJSON(res, 200, { ok: true, membership: updated }); +} + +export async function handleRemoveMember( + req: IncomingMessage, res: ServerResponse, teamId: string, userId: string, +): Promise { + const session = requireAuth(req, res); + if (!session) return; + if (!await requireTeamRole(res, session, teamId, 'admin')) return; + const removed = await removeMembership(teamId, userId); + if (!removed) { sendJSON(res, 404, { error: 'Membership not found' }); return; } + sendJSON(res, 200, { ok: true }); +} diff --git a/v2/server/schemas/brief.ts b/v2/server/schemas/brief.ts new file mode 100644 index 0000000..ade24a7 --- /dev/null +++ b/v2/server/schemas/brief.ts @@ -0,0 +1,64 @@ +// Single source of truth for the V2 brief shape. +// Used by the API routes, the operator-app form, and the pipeline orchestrator. +import { z } from 'zod'; + +export const COMPETITOR = z.object({ + name: z.string().min(1), + handle: z.string().min(1), +}); + +export const AUDIENCE = z.object({ + primary: z.string().min(2), + secondary: z.string().optional(), + age_range: z.string().min(1), + gender: z.string().min(1), + interests: z.array(z.string().min(1)).min(3), +}); + +export const PLATFORM = z.enum(['tiktok']); // V2 ships TikTok-only; expand later if scope changes. + +export const BRIEF_INPUT = z.object({ + client_name: z.string().min(1), + category: z.string().min(1), + brand: z.object({ + name: z.string().min(1), + handle: z.string().min(1), + positioning: z.string().optional(), + }), + competitors: z.array(COMPETITOR).min(3).max(15), + audience: AUDIENCE, + geo: z.string().min(2), + language: z.string().default('en'), + business_question: z.string().refine( + (v) => v.split(/\s+/).filter(Boolean).length >= 8, + { message: 'business_question must be at least 8 words' }, + ), + kpis: z.array(z.string().min(1)).min(2), + budget_usd: z.number().positive().min(10), + date_window_days: z.number().int().positive().default(30), + platforms: z.array(PLATFORM).min(1).default(['tiktok']), + context_vision: z.string().optional(), + prior_report_id: z.string().uuid().nullable().optional(), + + // The V2 quality knobs — adaptable per client. + min_likes: z.number().int().nonnegative().default(1000), + min_plays: z.number().int().nonnegative().default(10000), + min_stl_pct: z.number().nonnegative().max(100).default(0), + + // For brief-driven slug; auto-generated if omitted. + slug: z.string().optional(), +}); + +export type BriefInput = z.infer; + +export function slugify(s: string): string { + return s.toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, '') + .slice(0, 60) || 'brief'; +} + +export function defaultBriefSlug(client_name: string, created_at: Date = new Date()): string { + const ym = `${created_at.getUTCFullYear()}-${String(created_at.getUTCMonth() + 1).padStart(2, '0')}`; + return `${slugify(client_name)}-${ym}`; +} diff --git a/v2/templates/.gitkeep b/v2/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/v2/templates/dashboard_template/.gitkeep b/v2/templates/dashboard_template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/v2/templates/dashboard_template/package.json b/v2/templates/dashboard_template/package.json new file mode 100644 index 0000000..c03d7b1 --- /dev/null +++ b/v2/templates/dashboard_template/package.json @@ -0,0 +1,7 @@ +{ + "name": "v2-dashboard-template", + "version": "0.0.0", + "private": true, + "description": "Per-report React/Vite dashboard scaffold. Phase F builds out the full template; this is a placeholder to unblock Docker builds in Phases A–E.", + "type": "module" +} diff --git a/v2/tsconfig.base.json b/v2/tsconfig.base.json new file mode 100644 index 0000000..e316e8e --- /dev/null +++ b/v2/tsconfig.base.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": false, + "declaration": false, + "sourceMap": false + } +} diff --git a/v2/tsconfig.json b/v2/tsconfig.json new file mode 100644 index 0000000..54b356e --- /dev/null +++ b/v2/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "types": ["node", "vitest/globals"] + }, + "include": ["server/**/*.ts", "pipeline/**/*.ts"], + "exclude": ["node_modules", "dist", "operator-app", "templates"] +} diff --git a/v2/vitest.config.ts b/v2/vitest.config.ts new file mode 100644 index 0000000..fe572d2 --- /dev/null +++ b/v2/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['server/**/__tests__/**/*.test.ts', 'pipeline/**/__tests__/**/*.test.ts'], + environment: 'node', + globals: true, + }, +});