Files
sandbox/internal/api/openapi.yaml
pptx704 931b7d54b3 Add GitHub OAuth login with provider registry
Implement OAuth 2.0 login via GitHub as an alternative to email/password.
Uses a provider registry pattern (internal/auth/oauth/) so adding Google
or other providers later requires only a new Provider implementation.

Flow: GET /v1/auth/oauth/github redirects to GitHub, callback exchanges
the code for a user profile, upserts the user + team atomically, and
redirects to the frontend with a JWT token.

Key changes:
- Migration: make password_hash nullable, add oauth_providers table
- Provider registry with GitHubProvider (profile + email fallback)
- CSRF state cookie with HMAC-SHA256 validation
- Race-safe registration (23505 collision retries as login)
- Startup validation: CP_PUBLIC_URL required when OAuth is configured

Not fully tested — needs integration tests with a real GitHub OAuth app
and end-to-end testing with the frontend callback page.
2026-03-15 06:31:58 +06:00

950 lines
24 KiB
YAML

openapi: "3.1.0"
info:
title: Wrenn Sandbox API
description: MicroVM-based code execution platform API.
version: "0.1.0"
servers:
- url: http://localhost:8080
description: Local development
security: []
paths:
/v1/auth/signup:
post:
summary: Create a new account
operationId: signup
tags: [auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SignupRequest"
responses:
"201":
description: Account created
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"400":
description: Invalid request (bad email, short password)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Email already registered
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/auth/login:
post:
summary: Log in with email and password
operationId: login
tags: [auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
responses:
"200":
description: Login successful
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"401":
description: Invalid credentials
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/auth/oauth/{provider}:
parameters:
- name: provider
in: path
required: true
schema:
type: string
enum: [github]
description: OAuth provider name
get:
summary: Start OAuth login flow
operationId: oauthRedirect
tags: [auth]
description: |
Redirects the user to the OAuth provider's authorization page.
Sets a short-lived CSRF state cookie for validation on callback.
responses:
"302":
description: Redirect to provider authorization URL
"404":
description: Provider not found or not configured
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/auth/oauth/{provider}/callback:
parameters:
- name: provider
in: path
required: true
schema:
type: string
enum: [github]
description: OAuth provider name
get:
summary: OAuth callback
operationId: oauthCallback
tags: [auth]
description: |
Handles the OAuth provider's callback after user authorization.
Exchanges the authorization code for a user profile, creates or
logs in the user, and redirects to the frontend with a JWT token.
**On success:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback?token=...&user_id=...&team_id=...&email=...`
**On error:** redirects to `{OAUTH_REDIRECT_URL}/auth/{provider}/callback?error=...`
Possible error codes: `access_denied`, `invalid_state`, `missing_code`,
`exchange_failed`, `email_taken`, `internal_error`.
parameters:
- name: code
in: query
schema:
type: string
description: Authorization code from the OAuth provider
- name: state
in: query
schema:
type: string
description: CSRF state parameter (must match the cookie)
responses:
"302":
description: Redirect to frontend with token or error
/v1/api-keys:
post:
summary: Create an API key
operationId: createAPIKey
tags: [api-keys]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateAPIKeyRequest"
responses:
"201":
description: API key created (plaintext key only shown once)
content:
application/json:
schema:
$ref: "#/components/schemas/APIKeyResponse"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
summary: List API keys for your team
operationId: listAPIKeys
tags: [api-keys]
security:
- bearerAuth: []
responses:
"200":
description: List of API keys (plaintext keys are never returned)
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/APIKeyResponse"
/v1/api-keys/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
delete:
summary: Delete an API key
operationId: deleteAPIKey
tags: [api-keys]
security:
- bearerAuth: []
responses:
"204":
description: API key deleted
/v1/sandboxes:
post:
summary: Create a sandbox
operationId: createSandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSandboxRequest"
responses:
"201":
description: Sandbox created
content:
application/json:
schema:
$ref: "#/components/schemas/Sandbox"
"502":
description: Host agent error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
summary: List sandboxes for your team
operationId: listSandboxes
tags: [sandboxes]
security:
- apiKeyAuth: []
responses:
"200":
description: List of sandboxes
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Sandbox"
/v1/sandboxes/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Get sandbox details
operationId: getSandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
responses:
"200":
description: Sandbox details
content:
application/json:
schema:
$ref: "#/components/schemas/Sandbox"
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
summary: Destroy a sandbox
operationId: destroySandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
responses:
"204":
description: Sandbox destroyed
/v1/sandboxes/{id}/exec:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Execute a command
operationId: execCommand
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecRequest"
responses:
"200":
description: Command output
content:
application/json:
schema:
$ref: "#/components/schemas/ExecResponse"
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/ping:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Reset sandbox inactivity timer
operationId: pingSandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Resets the last_active_at timestamp for a running sandbox, preventing
the auto-pause TTL from expiring. Use this as a keepalive for sandboxes
that are idle but should remain running.
responses:
"204":
description: Ping acknowledged, inactivity timer reset
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/pause:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Pause a running sandbox
operationId: pauseSandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Takes a snapshot of the sandbox (VM state + memory + rootfs), then
destroys all running resources. The sandbox exists only as files on
disk and can be resumed later.
responses:
"200":
description: Sandbox paused (snapshot taken, resources released)
content:
application/json:
schema:
$ref: "#/components/schemas/Sandbox"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/resume:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Resume a paused sandbox
operationId: resumeSandbox
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Restores a paused sandbox from its snapshot using UFFD for lazy
memory loading. Boots a fresh Firecracker process, sets up a new
network slot, and waits for envd to become ready.
responses:
"200":
description: Sandbox resumed (new VM booted from snapshot)
content:
application/json:
schema:
$ref: "#/components/schemas/Sandbox"
"409":
description: Sandbox not paused
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/snapshots:
post:
summary: Create a snapshot template
operationId: createSnapshot
tags: [snapshots]
security:
- apiKeyAuth: []
description: |
Pauses a running sandbox, takes a full snapshot, copies the snapshot
files to the images directory as a reusable template, then destroys
the sandbox. The template can be used to create new sandboxes.
parameters:
- name: overwrite
in: query
required: false
schema:
type: string
enum: ["true"]
description: Set to "true" to overwrite an existing snapshot with the same name.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSnapshotRequest"
responses:
"201":
description: Snapshot created
content:
application/json:
schema:
$ref: "#/components/schemas/Template"
"409":
description: Name already exists or sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
summary: List templates for your team
operationId: listSnapshots
tags: [snapshots]
security:
- apiKeyAuth: []
parameters:
- name: type
in: query
required: false
schema:
type: string
enum: [base, snapshot]
description: Filter by template type.
responses:
"200":
description: List of templates
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Template"
/v1/snapshots/{name}:
parameters:
- name: name
in: path
required: true
schema:
type: string
delete:
summary: Delete a snapshot template
operationId: deleteSnapshot
tags: [snapshots]
security:
- apiKeyAuth: []
description: Removes the snapshot files from disk and deletes the database record.
responses:
"204":
description: Snapshot deleted
"404":
description: Template not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/write:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Upload a file
operationId: uploadFile
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [path, file]
properties:
path:
type: string
description: Absolute destination path inside the sandbox
file:
type: string
format: binary
description: File content
responses:
"204":
description: File uploaded
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"413":
description: File too large
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/read:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Download a file
operationId: downloadFile
tags: [sandboxes]
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ReadFileRequest"
responses:
"200":
description: File content
content:
application/octet-stream:
schema:
type: string
format: binary
"404":
description: Sandbox or file not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/exec/stream:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: Stream command execution via WebSocket
operationId: execStream
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Opens a WebSocket connection for streaming command execution.
**Client sends** (first message to start the process):
```json
{"type": "start", "cmd": "tail", "args": ["-f", "/var/log/syslog"]}
```
**Client sends** (to stop the process):
```json
{"type": "stop"}
```
**Server sends** (process events as they arrive):
```json
{"type": "start", "pid": 1234}
{"type": "stdout", "data": "line of output\n"}
{"type": "stderr", "data": "warning message\n"}
{"type": "exit", "exit_code": 0}
{"type": "error", "data": "description of error"}
```
The connection closes automatically after the process exits.
responses:
"101":
description: WebSocket upgrade
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/stream/write:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Upload a file (streaming)
operationId: streamUploadFile
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Streams file content to the sandbox without buffering in memory.
Suitable for large files. Uses the same multipart/form-data format
as the non-streaming upload endpoint.
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [path, file]
properties:
path:
type: string
description: Absolute destination path inside the sandbox
file:
type: string
format: binary
description: File content
responses:
"204":
description: File uploaded
"404":
description: Sandbox not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes/{id}/files/stream/read:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Download a file (streaming)
operationId: streamDownloadFile
tags: [sandboxes]
security:
- apiKeyAuth: []
description: |
Streams file content from the sandbox without buffering in memory.
Suitable for large files. Returns raw bytes with chunked transfer encoding.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ReadFileRequest"
responses:
"200":
description: File content streamed in chunks
content:
application/octet-stream:
schema:
type: string
format: binary
"404":
description: Sandbox or file not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"409":
description: Sandbox not running
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
securitySchemes:
apiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: API key for sandbox lifecycle operations. Create via POST /v1/api-keys.
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT token from /v1/auth/login or /v1/auth/signup. Valid for 6 hours.
schemas:
SignupRequest:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
LoginRequest:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
AuthResponse:
type: object
properties:
token:
type: string
description: JWT token (valid for 6 hours)
user_id:
type: string
team_id:
type: string
email:
type: string
CreateAPIKeyRequest:
type: object
properties:
name:
type: string
default: Unnamed API Key
APIKeyResponse:
type: object
properties:
id:
type: string
team_id:
type: string
name:
type: string
key_prefix:
type: string
description: Display prefix (e.g. "wrn_ab12cd34...")
created_at:
type: string
format: date-time
last_used:
type: string
format: date-time
nullable: true
key:
type: string
description: Full plaintext key. Only returned on creation, never again.
nullable: true
CreateSandboxRequest:
type: object
properties:
template:
type: string
default: minimal
vcpus:
type: integer
default: 1
memory_mb:
type: integer
default: 512
timeout_sec:
type: integer
default: 0
description: >
Auto-pause TTL in seconds. The sandbox is automatically paused
after this duration of inactivity (no exec or ping). 0 means
no auto-pause.
Sandbox:
type: object
properties:
id:
type: string
status:
type: string
enum: [pending, running, paused, stopped, error]
template:
type: string
vcpus:
type: integer
memory_mb:
type: integer
timeout_sec:
type: integer
guest_ip:
type: string
host_ip:
type: string
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
nullable: true
last_active_at:
type: string
format: date-time
nullable: true
last_updated:
type: string
format: date-time
CreateSnapshotRequest:
type: object
required: [sandbox_id]
properties:
sandbox_id:
type: string
description: ID of the running sandbox to snapshot.
name:
type: string
description: Name for the snapshot template. Auto-generated if omitted.
Template:
type: object
properties:
name:
type: string
type:
type: string
enum: [base, snapshot]
vcpus:
type: integer
nullable: true
memory_mb:
type: integer
nullable: true
size_bytes:
type: integer
format: int64
created_at:
type: string
format: date-time
ExecRequest:
type: object
required: [cmd]
properties:
cmd:
type: string
args:
type: array
items:
type: string
timeout_sec:
type: integer
default: 30
ExecResponse:
type: object
properties:
sandbox_id:
type: string
cmd:
type: string
stdout:
type: string
stderr:
type: string
exit_code:
type: integer
duration_ms:
type: integer
encoding:
type: string
enum: [utf-8, base64]
description: Output encoding. "base64" when stdout/stderr contain binary data.
ReadFileRequest:
type: object
required: [path]
properties:
path:
type: string
description: Absolute file path inside the sandbox
Error:
type: object
properties:
error:
type: object
properties:
code:
type: string
message:
type: string