Skip to content

Integrations Hub — Graphy, EzExam, Judge0, Google Drive, Terminal Relay

This document is the canonical reference for the cluster of third-party and infrastructure integrations in mr-mentor-backend that do not belong to a single product domain: the Graphy / Mr Learn LMS connector, the EzExam / Mr Test online-exam connector, the Judge0 code-execution proxy (powering coding quizzes), the Google Drive resume connector, the Terminal Relay WebSocket server (in-browser terminals running on a student's own machine), and the Platform registry (external job-board + recruitment-platform metadata + per-HR accounts). Each integration is a thin server-side adapter that bridges MAS to an external system whose product offers no first-class SDK, so we proxy its native HTTP/cookie APIs and surface them in our admin panels and portals.

Status: documented from source on this branch.


Overview

These integrations share one trait: the external product exposes no embed SDK or public API, so MAS authenticates as a service/staff account, replays the product's own browser API calls server-side, and re-shapes the result for our UIs. They are grouped here because none owns a full product domain — they are connectors and infra glue.

Integration External system What we do Primary persona
Graphy / Mr Learn mrlearn.in (Graphy LMS) Proxy storefront /s/ + teacher /t/api/ browser APIs; cached sync of learners/courses Admin (/admin/mrlearn)
EzExam / Mr Test *.ezexam.in (Django) Proxy /staff/* + /students/* APIs; cached sync of exams/submissions Admin (/admin/mrtest)
Judge0 Self-hosted Judge0 (:2358) Stateless proxy for code submission + test-case runner Public quiz-takers (students)
Google Drive Google Drive API v3 Per-user OAuth; list/search/download resume files for recruitment HR / recruiters (Mr Hire)
Terminal Relay Student's own laptop via @myanalyticsschool/connect CLI WebSocket byte-bridge between a student browser and a CLI on the student's machine Students (in-browser terminal/IDE)
Platform registry Job boards (Google Jobs, Indeed, LinkedIn, Naukri, …) Master platform list + per-HR connected accounts + per-job posting tracker; public JSON-LD / XML feeds HR + Admin + crawlers

The Graphy and EzExam connectors have a cached-sync companion layer (MrLearnSyncController / MrTestSyncController and the mrlearn.* / mrtest.* Postgres schemas). The live-proxy half is documented here; the deep sync pipeline (configs, runs, reminders, AI-training joins) is covered in integration-mr-learn.md. This doc focuses on the integration mechanics (auth, proxy, request/response shapes).


Key concepts & entities

Glossary

  • Live proxy — an endpoint that authenticates with the external system on demand and forwards the response with no persistence. Graphy /proxy, EzExam /proxy, all Judge0 routes, and Google Drive file ops are live proxies.
  • Cached sync — a background snapshot of external data into our mrlearn.* / mrtest.* schemas (see integration-mr-learn.md).
  • Service account — a single shared login (one row in mrlearn.auth_credentials / mrtest.auth_credentials) used by the connector to act on behalf of MAS. Not a per-user credential.
  • Microservice token — Graphy's teacher-panel Authorization: Basic … header, fetched from /s/microservice/token after cookie login; required for all /t/api/* calls.
  • csrfmiddlewaretoken — EzExam's Django CSRF form token; harvested from the /login HTML and sent on every mutating call.
  • Relay token — a short-lived (mas_term_…, 5 min TTL) opaque token presented by both the student browser and the CLI to pair them on the relay WebSocket.
  • Peer kindcli or browser; the two sides of a relay session.
  • Platform method — how a job board accepts postings: automatic (we emit a feed), oauth / api_key (HR connects an account), or manual.

Owned TypeORM entities

Entity File Schema / table Purpose
MrLearnAuthCredentials src/entities/mrlearn/MrLearnAuthCredentials.ts mrlearn.auth_credentials Graphy service-account creds + session (cUjwt, sessionId, microserviceToken, encrypted password)
MrTestAuthCredentials src/entities/mrtest/MrTestAuthCredentials.ts mrtest.auth_credentials EzExam staff-account creds + Django session (sessionId, csrfToken, csrfFormToken, encrypted password)
GoogleAuthTokens src/entities/GoogleAuthTokens.ts google_auth_tokens Per-user Google OAuth tokens (access_token, refresh_token, expiry_date, scope)
Platform src/entities/Platform.ts platforms Master job-board / recruitment-platform registry
HrPlatformAccount src/entities/HrPlatformAccount.ts hr_platform_accounts Per-HR connected account on a platform (encrypted credentials)
JobPlatformPosting src/entities/JobPlatformPosting.ts job_platform_postings One job posted to one platform (status, posted-at, external URL)

The Terminal Relay and Judge0 integrations own no Postgres entities. Relay session tokens live only in Redis (tterm:tok:*, tterm:bind:*, 5-min TTL). Judge0 is fully stateless. ExternalCourse (src/entities/ExternalCourse.ts) is part of the cached-sync layer, not the live proxy — mentioned for completeness.


Architecture

flowchart TD
  subgraph FE["Frontends"]
    ADMIN["mr-mentor-frontend admin panel"]
    HIRE["mr-hire-frontend recruiters"]
    QUIZ["Public quiz pages"]
    PORTAL["mas-website-live student portal"]
    CRAWLER["Google / Indeed crawlers"]
  end

  subgraph API["mr-mentor-backend routes"]
    RG["/api/graphy/*"]
    RE["/api/ezexam/*"]
    RJ["/api/judge0/*"]
    RGD["/api/google-drive-connector/*"]
    RT["/api/terminal/session"]
    RWS["WS /api/terminal/relay"]
    RP["/api hr+admin platforms"]
  end

  subgraph CTRL["Controllers"]
    CG["GraphyLmsController"]
    CE["EzExamController"]
    CJ["Judge0Controller"]
    CGD["GoogleDriveConnectorController"]
    CT["TerminalSessionController"]
    CP["PlatformController"]
  end

  subgraph SVC["Services"]
    SG["GraphyLmsService (singleton)"]
    SE["EzExamService (singleton)"]
    SGD["GoogleDriveConnectorService"]
    ST["TerminalRelayService"]
    STW["TerminalRelayWsServer"]
    SP["PlatformService"]
  end

  subgraph STORE["Persistence"]
    PG[("PostgreSQL")]
    REDIS[("Redis")]
  end

  subgraph EXT["External systems"]
    GRAPHY["mrlearn.in (Graphy)"]
    EZ["*.ezexam.in (Django)"]
    J0["Judge0 :2358"]
    GD["Google Drive API v3"]
    CLI["@myanalyticsschool/connect CLI on student laptop"]
  end

  ADMIN --> RG --> CG --> SG --> GRAPHY
  ADMIN --> RE --> CE --> SE --> EZ
  QUIZ --> RJ --> CJ --> J0
  HIRE --> RGD --> CGD --> SGD --> GD
  PORTAL --> RT --> CT --> ST
  PORTAL -. websocket .-> RWS
  CLI -. websocket .-> RWS
  RWS --> STW --> ST
  HIRE --> RP --> CP --> SP
  CRAWLER --> RP

  SG --> PG
  SE --> PG
  SGD --> PG
  SP --> PG
  ST --> REDIS

Data model

erDiagram
  PLATFORM ||--o{ HR_PLATFORM_ACCOUNT : "connected by HR"
  PLATFORM ||--o{ JOB_PLATFORM_POSTING : "target of"
  HR_PLATFORM_ACCOUNT ||--o{ JOB_PLATFORM_POSTING : "posts via"
  USER ||--o{ HR_PLATFORM_ACCOUNT : "owns"
  USER ||--o{ GOOGLE_AUTH_TOKENS : "authorizes"
  JOB_POST ||--o{ JOB_PLATFORM_POSTING : "published to"

  PLATFORM {
    varchar id PK
    varchar name
    varchar method "automatic|oauth|api_key|manual"
    varchar category "job_board|social|portal"
    boolean isActive
    jsonb authFields
    text baseUrl
    text docsUrl
  }
  HR_PLATFORM_ACCOUNT {
    uuid id PK
    uuid userId FK
    varchar platformId FK
    text credentials "AES-256 encrypted JSON"
    boolean isConnected
    varchar connectionStatus "disconnected|connected|expired|error"
    timestamp lastVerifiedAt
    jsonb metadata
  }
  JOB_PLATFORM_POSTING {
    uuid id PK
    uuid jobPostId FK
    varchar platformId FK
    uuid hrPlatformAccountId FK
    varchar method
    varchar status "pending|posted|failed|manual_required"
    timestamp postedAt
    text externalUrl
    int applicationsReceived
  }
  GOOGLE_AUTH_TOKENS {
    uuid id PK
    uuid userId FK
    text access_token
    text refresh_token
    bigint expiry_date
    varchar scope
  }
  MRLEARN_AUTH_CREDENTIALS {
    uuid id PK
    varchar email UK
    text encryptedPassword
    text cUjwt
    text sessionId
    text microserviceToken
    timestamp authenticatedAt
  }
  MRTEST_AUTH_CREDENTIALS {
    uuid id PK
    varchar username UK
    text encryptedPassword
    text sessionId
    text csrfToken
    text csrfFormToken
    timestamp authenticatedAt
  }

MRLEARN_AUTH_CREDENTIALS and MRTEST_AUTH_CREDENTIALS are standalone single-row tables (one service account each) with no FK relationships — shown separately above. The relay session token has no table; it is a Redis record (TerminalSessionRecord = studentId, sceneId, mode, createdAt).


API surface

Paths below are the full mounted paths (mount prefix + route file), derived from src/routes/index.ts.

Graphy / Mr Learn — /api/graphy (all routes authMiddleware + adminMiddleware)

Method Path Auth/role Purpose
POST /api/graphy/auth/login Admin Authenticate the Graphy service account (email/password or forceRefresh)
GET /api/graphy/auth/status Admin Report cached auth state (authenticated, email, hasBasicAuth)
POST /api/graphy/auth/logout Admin Clear stored Graphy auth
GET /api/graphy/courses Admin List Graphy courses (/s/courses/all)
GET /api/graphy/users/students Admin List learners (/t/api/user/get)
GET /api/graphy/users/admins Admin List course-admins/sub-admins
GET /api/graphy/registrable-students Admin Enrolled MAS students not yet on Mr Learn
POST /api/graphy/learners Admin Create a learner on Mr Learn (/t/api/user/create)
GET /api/graphy/questions Admin Question-bank listing (/s/questions)
GET /api/graphy/courses/:courseId/learners Admin Course learners + progress
POST /api/graphy/courses/:courseId/export Admin Trigger CSV report export
GET /api/graphy/users/:userId/courses/:courseId/report Admin Per-learner course report
GET /api/graphy/reports Admin List generated CSV reports
GET /api/graphy/reports/:reportId/download Admin Presigned download URL for a report
POST /api/graphy/proxy Admin Generic pass-through to any Graphy /s/ endpoint
/api/graphy/sync/* Admin Cached-sync layer — see integration-mr-learn.md

EzExam / Mr Test — /api/ezexam (all routes authMiddleware + adminMiddleware)

Method Path Auth/role Purpose
POST /api/ezexam/auth/login Admin Authenticate the EzExam staff account
GET /api/ezexam/auth/status Admin Report cached EzExam auth state
POST /api/ezexam/auth/logout Admin Log out + clear EzExam session
GET /api/ezexam/registrable-students Admin Enrolled MAS students not yet on Mr Test
POST /api/ezexam/students Admin Create a student on Mr Test (/staff/add_stud)
GET /api/ezexam/capacity Admin Account capacity (/staff/get_cap)
GET /api/ezexam/question-papers Admin Question papers (/staff/get_qpaps)
GET /api/ezexam/online-exams Admin Online exams (/staff/get_onexs)
GET /api/ezexam/online-exams/:onexId/detail Admin Submissions + results + students + content bundle
POST /api/ezexam/online-exams/:onexId/regenerate-analytics Admin Trigger EzExam analytics rebuild (/staff/gen_onex_anal)
POST /api/ezexam/proxy Admin Generic pass-through to any /staff/* or /admin/* endpoint
/api/ezexam/sync/*, /api/ezexam/training/* Admin Cached-sync + AI-training layer — see integration-mr-learn.md

Judge0 — /api/judge0 (called from public quiz pages)

Method Path Auth/role Purpose
POST /api/judge0/submissions Public Create a code submission (proxied with wait=true)
GET /api/judge0/submissions/:token Public Fetch submission result by token
GET /api/judge0/languages Public List supported languages
POST /api/judge0/run-tests Public Run source against an array of test cases, return pass/fail summary

Google Drive connector — /api/google-drive-connector

Method Path Auth/role Purpose
GET /api/google-drive-connector/auth-url Authed user Build the Google OAuth consent URL (state=userId)
GET /api/google-drive-connector/callback None (Google redirect) Exchange code→tokens, store, redirect to Mr Hire frontend
GET /api/google-drive-connector/status Authed user Is this user's Drive connected
GET /api/google-drive-connector/files Authed user List files (folderId?, pageSize?)
GET /api/google-drive-connector/search Authed user Search resume-like files (q?)
GET /api/google-drive-connector/download/:fileId Authed user Download file as base64
DELETE /api/google-drive-connector/disconnect Authed user Remove stored Drive tokens
POST /api/google-drive-connector/import Authed user Import selected files (resume → candidate flow)

Terminal session + relay — /api

Method Path Auth/role Purpose
POST /api/terminal/session Authed student Mint a relay token + return the relay WS URL
WS /api/terminal/relay Token in first frame Pair a browser peer with a CLI peer and bridge bytes

Platform registry — /api

Method Path Auth/role Purpose
GET /api/careers/google-jobs/:id Public Google-for-Jobs JSON-LD for one job
GET /api/careers/google-jobs Public JSON-LD for all jobs
GET /api/careers/indeed-feed.xml Public Indeed XML job feed
GET /api/hr/platforms Authed HR List active platforms
GET /api/hr/platform-accounts Authed HR List my connected accounts (credentials stripped)
POST /api/hr/platform-accounts Authed HR Connect/encrypt an account
PUT /api/hr/platform-accounts/:id Authed HR Update account credentials
DELETE /api/hr/platform-accounts/:id Authed HR Disconnect an account
POST /api/hr/job-posts/:id/publish Authed HR Publish a job to selected platforms
GET /api/hr/job-posts/:id/platforms Authed HR List a job's postings
PATCH /api/hr/job-posts/:id/platforms/:platformId Authed HR Mark a posting as posted (optional external URL)
GET /api/admin/platforms Admin List all platforms
POST /api/admin/platforms Admin Create a platform
PUT /api/admin/platforms/:id Admin Update a platform
PATCH /api/admin/platforms/:id/toggle Admin Toggle active state

HR call config (mentioned) — /api

The HR-call-config routes (src/routes/hrCallConfig.routes.ts, mounted at /api) are related infra: GET /api/hr-call-config/:userId is public (read by mr-hire-backend, credentials stripped); GET/PUT /api/hr/my-call-config are HR self-service; /api/admin/hr-call-configs/* are admin. The full voice-calling provider story lives in mr-hire-voice-interviews-and-ai-calling.md.


User journeys

1. Graphy LMS connector — authenticate then proxy a course list

The admin opens /admin/mrlearn. The connector logs in once with the Graphy service account, captures cookies + the microservice token, persists them, and then proxies storefront/teacher calls. Auth state is cached in memory, then Postgres (mrlearn.auth_credentials), surviving restarts.

sequenceDiagram
  participant Admin as Admin Panel
  participant API as GraphyLmsController
  participant SVC as GraphyLmsService
  participant DB as Postgres mrlearn
  participant GR as mrlearn.in

  Admin->>API: POST /api/graphy/auth/login with email and password
  API->>SVC: authenticate email password
  SVC->>SVC: check in-memory then loadFromStore
  alt no valid session
    SVC->>GR: POST /s/authenticate form encoded
    GR-->>SVC: 302 with Set-Cookie c_ujwt and SESSIONID
    SVC->>GR: GET /s/microservice/token with cookies
    GR-->>SVC: token value
    SVC->>DB: upsert encrypted password and session
  end
  SVC-->>API: auth state
  API-->>Admin: authenticated true with email

  Admin->>API: GET /api/graphy/courses
  API->>SVC: getCourses
  SVC->>GR: GET /s/courses/all with cookies
  alt 401 or 403
    SVC->>GR: re-authenticate once then retry
  end
  GR-->>SVC: courses JSON
  SVC-->>API: courses
  API-->>Admin: success with data

Notes

  • Storefront calls (/s/*) use cookie headers only; teacher calls (/t/api/*) additionally send Authorization: <microserviceToken> via buildTeacherHeaders.
  • On any 401/403 the service re-authenticates once with the cached password and retries; if the cached password fails to decrypt (rotated JWT_SECRET), the admin is re-prompted.
  • GET /api/graphy/registrable-students runs a raw SQL join against mrlearn.learners to find enrolled MAS students missing on Mr Learn (de-prefixing the Graphy email alias).

2. Graphy — create a learner (push a MAS student into Mr Learn)

sequenceDiagram
  participant Admin as Admin Panel
  participant API as GraphyLmsController
  participant SVC as GraphyLmsService
  participant GR as mrlearn.in teacher API

  Admin->>API: GET /api/graphy/registrable-students
  API->>API: raw SQL join users enrolled minus mrlearn learners
  API-->>Admin: list of candidates
  Admin->>API: POST /api/graphy/learners name email phone password
  API->>SVC: createLearner normalise phone to plus91
  SVC->>GR: POST /t/api/user/create role student
  GR-->>SVC: created learner payload
  SVC-->>API: data
  API-->>Admin: success

3. EzExam admin integration — login then fetch an exam detail bundle

EzExam is a Django app. Login is a two-step CSRF dance: GET /login to harvest the csrftoken cookie + inline csrfmiddlewaretoken, then POST credentials. Subsequent /staff/* calls reuse both cookies; mutating calls send the CSRF token in the body.

sequenceDiagram
  participant Admin as Admin Panel
  participant API as EzExamController
  participant SVC as EzExamService
  participant DB as Postgres mrtest
  participant EZ as ezexam.in Django

  Admin->>API: POST /api/ezexam/auth/login username password
  API->>SVC: authenticate
  SVC->>EZ: GET /login
  EZ-->>SVC: Set-Cookie csrftoken plus inline csrfmiddlewaretoken
  SVC->>EZ: POST /login with form token and cookie
  EZ-->>SVC: login_status OK plus Set-Cookie sessionid
  SVC->>DB: upsert sessionid csrftoken encrypted password
  SVC-->>API: auth state
  API-->>Admin: authenticated

  Admin->>API: GET /api/ezexam/online-exams/:onexId/detail
  API->>SVC: getSubmissions getResults getOnlineExamStudents getOnlineExamContent
  SVC->>EZ: POST /staff/get_subm and get_res and get_onex_studs with onexid
  alt 401 or 403
    SVC->>SVC: re-auth once with cached password then retry
  end
  EZ-->>SVC: JSON bundles
  SVC-->>API: merged detail
  API-->>Admin: exam detail

Notes

  • The wire field is onexid (one word). request() sends the unmasked cookie csrftoken value as both the X-CSRFToken header and the csrfmiddlewaretoken form field — Django's unmask step becomes a no-op so the connector never has to refresh the per-render form token.
  • A few endpoints require multipart (get_studs_paging, add_*) or url-encoded (download_studs) bodies; EzExamService has dedicated multipartRequest / urlEncodedRequest helpers.
  • Pages with no JSON sibling (/students/:rollnum/report, /students/:roll/analysis/:qpapId) are fetched as HTML and parsed with a tiny regex table parser (parseStudentReportHtml). The students CSV (download_studs) is the only upstream surface carrying student emails.
  • regenerate-analytics returns immediately; the client must re-poll get_res after a short delay (EzExam computes async on their side).

4. Judge0 — coding-quiz submission and result

Public quiz pages call the Judge0 proxy directly. Because the proxy submits with wait=true, the result is returned synchronously in one round-trip (no client polling needed) — but the classic submit/poll fallback also exists via the token endpoint.

sequenceDiagram
  participant Quiz as Public Quiz Page
  participant API as Judge0Controller
  participant J0 as Judge0 :2358

  Quiz->>API: POST /api/judge0/submissions source_code language_id stdin
  API->>API: validate source_code and language_id present
  API->>J0: POST /submissions base64_encoded false wait true
  Note over J0: Judge0 compiles and runs synchronously
  J0-->>API: submission with stdout status time memory
  API-->>Quiz: forwarded JSON with same HTTP status
  opt async fallback by token
    Quiz->>API: GET /api/judge0/submissions/:token
    API->>J0: GET /submissions/:token
    J0-->>API: current state
    API-->>Quiz: result
  end

Sequence for the test-case runner (/run-tests), used to grade coding questions against expected outputs:

sequenceDiagram
  participant Quiz as Public Quiz Page
  participant API as Judge0Controller
  participant J0 as Judge0 :2358

  Quiz->>API: POST /api/judge0/run-tests source_code language_id testCases
  API->>API: validate testCases is non empty array
  loop each test case in parallel
    API->>J0: POST /submissions wait true with stdin from test input
    J0-->>API: stdout and status id
    API->>API: passed when status id is 3 and trimmed stdout equals expected
  end
  API-->>Quiz: totalTestCases passed failed and per case results
  alt Judge0 unreachable
    API-->>Quiz: 502 failed to reach Judge0 API
  end

status.id === 3 is Judge0's "Accepted" status; the runner trims whitespace on both sides before comparing. See assessments-quizzes-assignments.md for how coding questions store source/language and consume these results.

5. Google Drive connector — OAuth then list/download/import resumes

A recruiter connects their own Google Drive (per-user OAuth, drive.readonly scope) to pull resume files into the recruitment pipeline.

sequenceDiagram
  participant HR as Mr Hire Frontend
  participant API as GoogleDriveConnectorController
  participant SVC as GoogleDriveConnectorService
  participant DB as google_auth_tokens
  participant GG as Google OAuth and Drive

  HR->>API: GET /api/google-drive-connector/auth-url
  API->>SVC: getAuthUrl userId in state
  SVC-->>API: consent URL
  API-->>HR: redirect user to Google
  HR->>GG: grant drive.readonly
  GG->>API: GET /api/google-drive-connector/callback code and state
  API->>SVC: handleOAuthCallback code userId
  SVC->>GG: exchange code for tokens
  GG-->>SVC: access_token refresh_token expiry
  SVC->>DB: upsert tokens for userId
  API-->>HR: redirect to candidates with drive connected

  HR->>API: GET /api/google-drive-connector/files folderId
  API->>SVC: listFiles
  SVC->>DB: load tokens
  alt token expired
    SVC->>GG: refreshAccessToken
    SVC->>DB: save refreshed token
  end
  SVC->>GG: drive.files.list trashed false in parent
  GG-->>SVC: files with shortcut resolution
  SVC-->>API: DriveFile list
  API-->>HR: files

  HR->>API: GET /api/google-drive-connector/download/:fileId
  API->>SVC: downloadFile
  SVC->>GG: files.get alt media arraybuffer
  GG-->>SVC: bytes
  SVC-->>API: base64 plus mimeType
  API-->>HR: file content

Notes

  • getAuthUrl requests access_type=offline + prompt=consent to guarantee a refresh token.
  • The OAuth callback is a Google redirect; the userId is carried in the OAuth state parameter. On success it redirects to MR_HIRE_FRONTEND_URL/candidates?drive=connected; on failure ?drive=error.
  • Token refresh is automatic when expiry_date < Date.now(); the refreshed token is persisted.
  • Shortcuts to folders are resolved to their target id/mimeType so navigation works.

6. Terminal relay — pair a student browser with the student's own machine

The student opens an in-browser terminal/IDE on a lesson page. The backend issues a short-lived token; the browser dials the relay WebSocket and the student pastes the same token into the @myanalyticsschool/connect CLI on their laptop. The relay pairs the two peers and bridges raw bytes — code runs on the student's machine, not ours.

sequenceDiagram
  participant Browser as Student Browser
  participant API as TerminalSessionController
  participant SVC as TerminalRelayService
  participant REDIS as Redis
  participant WS as TerminalRelayWsServer
  participant CLI as connect CLI on laptop

  Browser->>API: POST /api/terminal/session sceneId mode
  API->>SVC: issueToken studentId sceneId mode
  SVC->>REDIS: check bind key for live token
  alt existing live token
    REDIS-->>SVC: reuse token and remaining ttl
  else mint new
    SVC->>REDIS: set tok and bind keys ttl 300s
  end
  SVC-->>API: token and expiresAt
  API-->>Browser: token relayUrl expiresAt

  Browser->>WS: WS connect first frame auth with token peer browser
  WS->>SVC: resolveToken
  SVC->>REDIS: get tok record
  REDIS-->>SVC: record
  WS-->>Browser: control ready then park as first peer
  CLI->>WS: WS connect first frame auth with token peer cli
  WS->>SVC: resolveToken
  WS-->>CLI: control ready
  Note over WS: both peers present so bridge them
  WS-->>Browser: control peer-connected and peer-resize
  WS-->>CLI: control peer-connected and peer-resize
  loop interactive session
    Browser->>WS: binary keystrokes
    WS->>CLI: forward bytes unless read only
    CLI->>WS: binary stdout
    WS->>Browser: forward bytes
  end
  CLI->>WS: close
  WS-->>Browser: control peer-disconnect then close

Notes

  • The relay attaches to the same HTTP server as Socket.IO using noServer: true and a manual upgrade handler scoped to path /api/terminal/relay, so it does not collide with Socket.IO.
  • Token issuance is idempotent per (studentId, sceneId) via a reverse-index bind: key — React strict-mode double mounts, hot reloads, and refreshes all get the same live token instead of orphaning the CLI pairing.
  • The token is not consumed on read — both peers present it, so it lives until its 5-min TTL expires.
  • Read-only sessions: if the CLI handshakes with readOnly, the relay drops browser→CLI binary frames (defense in depth; the CLI also drops them).
  • Only whitelisted control messages are forwarded between peers: resize, close, ide-ready, ide-error (the last two are emitted by mas-connect-ide once its cloudflared tunnel is up, so the browser can iframe the IDE URL). See the browser-terminal-and-editor skill for the client side.
  • 30s heartbeat pings drop dead peers so dangling halves do not accumulate.

7. Platform registry — connect an HR account and publish a job

sequenceDiagram
  participant HR as Mr Hire Frontend
  participant API as PlatformController
  participant SVC as PlatformService
  participant DB as Postgres

  HR->>API: GET /api/hr/platforms
  API->>SVC: getActivePlatforms
  SVC->>DB: select platforms where isActive
  DB-->>API: platform list
  API-->>HR: platforms with authFields

  HR->>API: POST /api/hr/platform-accounts platformId credentials
  API->>SVC: connectAccount
  SVC->>SVC: encryptSecret credentials JSON
  SVC->>DB: upsert hr_platform_accounts connected
  SVC-->>API: account with credentials stripped
  API-->>HR: connected

  HR->>API: POST /api/hr/job-posts/:id/publish platformIds
  API->>SVC: publishToPlaftorms
  loop each platform
    alt method automatic
      SVC->>DB: posting status posted
    else has connected account
      SVC->>DB: posting status manual_required method manual
    else manual or no account
      SVC->>DB: posting status manual_required
    end
  end
  SVC-->>API: postings
  API-->>HR: published summary

8. Public crawler feeds (Google for Jobs / Indeed)

Search engines crawl public, unauthenticated endpoints. automatic-method platforms (Google Jobs, Indeed) are "published" simply by these feeds existing.

sequenceDiagram
  participant Crawler as Google or Indeed
  participant API as PlatformController

  Crawler->>API: GET /api/careers/google-jobs/:id
  API->>API: build JobPosting JSON-LD
  API-->>Crawler: application slash ld plus json
  Crawler->>API: GET /api/careers/indeed-feed.xml
  API->>API: build XML feed of active jobs
  API-->>Crawler: XML

Background jobs & async

  • No BullMQ queues are owned by the live-proxy half of these integrations. The cached-sync layer (/api/graphy/sync/*, /api/ezexam/sync/*, the new-student cron, WhatsApp reminders) does run scheduled syncs — documented in integration-mr-learn.md.
  • Socket-style transport: the Terminal Relay runs its own ws WebSocketServer (not Socket.IO) on /api/terminal/relay, attached in src/index.ts via attachTerminalRelay(httpServer). It maintains an in-memory pairings map and a 30s heartbeat setInterval.
  • Redis TTL expiry is the only timed job for relay tokens: tterm:tok:* and tterm:bind:* keys expire after 300s.
  • EzExam analytics is fire-and-forget: regenerate-analytics triggers async computation on EzExam's backend; clients re-poll.
  • Judge0 with wait=true is synchronous from our side; no polling queue.

External integrations

System Base URL env var Auth model Failure / fallback
Graphy (Mr Learn) GRAPHY_BASE_URL (default https://www.mrlearn.in) Service-account cookie login + microservice Basic token On 401/403 re-auth once + retry; decrypt failure → admin re-login; errors surface as 502
EzExam (Mr Test) EZEXAM_BASE_URL (default https://myanalyticsschool.ezexam.in) Django CSRF cookie/session, single staff account On 401/403 re-auth once with cached password; non-JSON login → "credentials likely invalid"
Judge0 JUDGE0_API_URL (default http://localhost:2358) None (internal service) Unreachable → 502 Failed to reach Judge0 API; per-test-case errors degrade to a failed test row
Google Drive GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_DRIVE_REDIRECT_URI; redirect target MR_HIRE_FRONTEND_URL Per-user OAuth (drive.readonly), auto-refresh Missing tokens → "User not authenticated with Google Drive"; callback failure → redirect ?drive=error
Terminal Relay CLI none (relay URL derived from request host / x-forwarded-*) Short-lived relay token (Redis, 5-min TTL) Bad/expired token → WS close 4003; duplicate same-side peer → 4009; malformed auth → 4001
Platform credentials PLATFORM_ENCRYPTION_KEY (32-byte; falls back to an insecure default) AES-256 encrypted JSON per HR account Credentials never returned in API responses; decrypt failure → null

Encryption-at-rest details

  • Graphy + EzExam passwords: AES-256-GCM keyed off sha256(JWT_SECRET). Rotating JWT_SECRET invalidates stored passwords (admin must re-login). Stored as iv:authTag:ciphertext hex.
  • HR platform credentials: encryptSecret/decryptSecret (src/utils/encryption.ts) keyed off PLATFORM_ENCRYPTION_KEY. Gotcha: the default key 'default-32-byte-key-change-in-prod!!' must be overridden in production.

Feature flags / self-disabling

  • HR call-config endpoints are public-readable (credentials stripped) so mr-hire-backend can read them; the broader telephony feature self-disables when its provider env vars are unset (see comms-telephony-exotel.md).

Status lifecycles

JobPlatformPosting.status

stateDiagram-v2
  [*] --> pending
  pending --> posted: automatic platform or markAsPosted
  pending --> manual_required: oauth or api_key or manual platform
  manual_required --> posted: HR marks as posted
  pending --> failed: updatePostingStatus error
  manual_required --> failed: updatePostingStatus error
  posted --> [*]

HrPlatformAccount.connectionStatus

stateDiagram-v2
  [*] --> disconnected
  disconnected --> connected: connectAccount
  connected --> connected: updateAccountCredentials
  connected --> disconnected: disconnectAccount
  connected --> expired: token lifetime ends
  connected --> error: verification failure
  expired --> connected: reconnect
  error --> connected: reconnect

Terminal relay session (token + WS lifecycle)

stateDiagram-v2
  [*] --> issued: POST terminal session mints token
  issued --> onePeerWaiting: first peer auth ok
  onePeerWaiting --> bridged: partner peer auth ok
  onePeerWaiting --> expired: 5 min TTL or peer closes
  bridged --> closed: either peer closes or errors
  issued --> expired: token TTL lapses unused
  closed --> [*]
  expired --> [*]

Edge cases, limits & gotchas

  • Single service account, not multi-tenant. Graphy and EzExam each operate with exactly one credential row. GraphyLmsService and EzExamService are module singletons so the cookie/token cache is shared across all admin requests. There is no x-platform routing for these connectors.
  • JWT_SECRET rotation breaks stored passwords. Both connectors key their AES password encryption off JWT_SECRET; rotating it forces an admin re-login (tokens are kept in case they still work, but the cached password no longer decrypts).
  • Graphy two-token model. /s/* storefront calls need only cookies; /t/api/* teacher calls additionally need the microservice Authorization header. A session can have valid cookies but a missing microserviceTokenbuildTeacherHeaders throws if so, forcing re-auth.
  • EzExam onexid spelling. The wire field is onexid (no underscore); an earlier onex_id was silently treated as missing by Django and returned empty results. Keep field names aligned with the real portal's calls.
  • EzExam CSRF shortcut. Sending the unmasked cookie csrftoken value (rather than the per-render masked form token) in both the header and body means the connector never needs to refresh the form token after the cookie rotates on login.
  • HTML scraping fragility. EzExam student report/analysis pages and Graphy's mixed content-types are parsed from HTML / text/plain with regex. Upstream markup changes will break parsing — these are the most brittle surfaces.
  • Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (internal/security-and-hardening-notes.md) and is intentionally not published on this site.
  • Judge0 wait=true blocking. The proxy blocks the request until Judge0 finishes; run-tests fans out one synchronous submission per test case in parallel (Promise.all), which can be heavy for large test suites.
  • Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (internal/security-and-hardening-notes.md) and is intentionally not published on this site.
  • Drive importFiles is a stub. GoogleDriveConnectorService.importFiles currently downloads and logs file size but does not yet POST to the application-creation endpoint (see the // Note: This would need to be sent… comment) — the actual candidate creation is a TODO.
  • Relay multi-replica caveat. Pairing requires both peers to land on the same server instance (in-memory pairings map). Sticky sessions make this fine for single-instance dev/staging; horizontal scaling would need Redis pub/sub routing (documented as future work in TerminalRelayWsServer.ts).
  • Relay first-frame contract. The first WS frame must be a JSON auth frame (type:"auth", token, optional peer); a binary first frame or malformed JSON closes with 4001. Peer kind is inferred from the version field when peer is omitted (CLIs send version, browsers do not).
  • Relay token reuse vs. orphaning. Minting a fresh token on every mount would orphan whichever token the CLI already paired with; the bind: reverse index prevents this.
  • Platform encryption default key. PLATFORM_ENCRYPTION_KEY falls back to a hard-coded 32-byte string — must be set in production or credentials are trivially decryptable.
  • publishToPlaftorms spelling. The service method is literally named publishToPlaftorms (typo) — keep in mind when grepping.