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/MrTestSyncControllerand themrlearn.*/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/tokenafter cookie login; required for all/t/api/*calls. - csrfmiddlewaretoken — EzExam's Django CSRF form token; harvested from the
/loginHTML 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 kind —
cliorbrowser; 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), ormanual.
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_CREDENTIALSandMRTEST_AUTH_CREDENTIALSare 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 sendAuthorization: <microserviceToken>viabuildTeacherHeaders. - On any
401/403the service re-authenticates once with the cached password and retries; if the cached password fails to decrypt (rotatedJWT_SECRET), the admin is re-prompted. GET /api/graphy/registrable-studentsruns a raw SQL join againstmrlearn.learnersto 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 cookiecsrftokenvalue as both theX-CSRFTokenheader and thecsrfmiddlewaretokenform 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;EzExamServicehas dedicatedmultipartRequest/urlEncodedRequesthelpers. - 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-analyticsreturns immediately; the client must re-pollget_resafter 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 === 3is 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
getAuthUrlrequestsaccess_type=offline+prompt=consentto guarantee a refresh token.- The OAuth callback is a Google redirect; the
userIdis carried in the OAuthstateparameter. On success it redirects toMR_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: trueand a manualupgradehandler scoped to path/api/terminal/relay, so it does not collide with Socket.IO. - Token issuance is idempotent per
(studentId, sceneId)via a reverse-indexbind: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 bymas-connect-ideonce its cloudflared tunnel is up, so the browser can iframe the IDE URL). See thebrowser-terminal-and-editorskill 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
wsWebSocketServer (not Socket.IO) on/api/terminal/relay, attached insrc/index.tsviaattachTerminalRelay(httpServer). It maintains an in-memorypairingsmap and a 30s heartbeatsetInterval. - Redis TTL expiry is the only timed job for relay tokens:
tterm:tok:*andtterm:bind:*keys expire after 300s. - EzExam analytics is fire-and-forget:
regenerate-analyticstriggers async computation on EzExam's backend; clients re-poll. - Judge0 with
wait=trueis 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). RotatingJWT_SECRETinvalidates stored passwords (admin must re-login). Stored asiv:authTag:ciphertexthex. - HR platform credentials:
encryptSecret/decryptSecret(src/utils/encryption.ts) keyed offPLATFORM_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-backendcan 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.
GraphyLmsServiceandEzExamServiceare module singletons so the cookie/token cache is shared across all admin requests. There is nox-platformrouting 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 microserviceAuthorizationheader. A session can have valid cookies but a missingmicroserviceToken—buildTeacherHeadersthrows if so, forcing re-auth. - EzExam
onexidspelling. The wire field isonexid(no underscore); an earlieronex_idwas 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
csrftokenvalue (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/plainwith 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=trueblocking. The proxy blocks the request until Judge0 finishes;run-testsfans 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
importFilesis a stub.GoogleDriveConnectorService.importFilescurrently 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
pairingsmap). Sticky sessions make this fine for single-instance dev/staging; horizontal scaling would need Redis pub/sub routing (documented as future work inTerminalRelayWsServer.ts). - Relay first-frame contract. The first WS frame must be a JSON auth frame (
type:"auth",token, optionalpeer); a binary first frame or malformed JSON closes with4001. Peer kind is inferred from theversionfield whenpeeris omitted (CLIs sendversion, 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_KEYfalls back to a hard-coded 32-byte string — must be set in production or credentials are trivially decryptable. publishToPlaftormsspelling. The service method is literally namedpublishToPlaftorms(typo) — keep in mind when grepping.
Related docs¶
- integration-mr-learn.md — the cached-sync layer for Graphy/Mr Learn + EzExam/Mr Test (configs, runs, reminders, AI-training joins).
- assessments-quizzes-assignments.md — coding quizzes that consume the Judge0 proxy.
- mr-hire-jobs-and-applications.md —
JobPostlifecycle thatJobPlatformPostingextends; Google Drive resume import target. - mr-hire-ai-resume-screening.md — downstream consumer of imported Drive resumes.
- mr-hire-voice-interviews-and-ai-calling.md — HR call-config provider story.
- comms-telephony-exotel.md — feature-flag / self-disable pattern referenced by HR call config.
- identity-and-access.md —
authMiddleware/adminMiddlewareand Google OAuth foundations shared by these connectors.