Skip to content

Latest commit

 

History

History
360 lines (264 loc) · 8.32 KB

File metadata and controls

360 lines (264 loc) · 8.32 KB

API Reference

All endpoints are Netlify Functions accessible at /.netlify/functions/<name>.

Endpoints Overview

Endpoint Method Purpose Reads Sheet?
/access-verify POST Verify code, bind device, create session Yes
/access-me GET Check current session validity No
/access-log POST Log usage events No (append only)

POST /access-verify

Verifies an access code, performs device binding, and creates a session.

Request

POST /.netlify/functions/access-verify
Content-Type: application/json

{
  "code": "ABCD-EFGH-IJKL-MNOP"
}

Body Parameters:

Field Type Required Description
code string Yes The access code to verify

Successful Response

HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: gate_device=...; HttpOnly; Secure; SameSite=None; Path=/; Max-Age=31536000
Set-Cookie: gate_access=...; HttpOnly; Secure; SameSite=None; Path=/; Max-Age=604800

{
  "ok": true,
  "label": "Acme Corp Demo",
  "expires_at": "2025-12-31T00:00:00.000Z"
}

Note: Cookie prefix is configurable via COOKIE_PREFIX env var (default: gate).

Response Fields:

Field Type Description
ok boolean Always true on success
label string Human-readable label from Access sheet
expires_at string ISO timestamp when access expires

Error Responses

Invalid or Missing Code

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "ok": false,
  "error": "INVALID_CODE"
}

Code Revoked

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "ok": false,
  "error": "REVOKED"
}

Code Expired

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "ok": false,
  "error": "EXPIRED"
}

Device Limit Reached (Code Already Bound to Max Devices)

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "ok": false,
  "error": "DEVICE_LIMIT_REACHED"
}

This occurs when the code has already been used on the maximum number of devices (default: 3, configurable per code via max_devices column).

Missing Expiration in Sheet

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "ok": false,
  "error": "MISSING_EXPIRES_AT"
}

Code Normalization

Before hashing, codes are normalized:

  1. Trimmed of whitespace
  2. Converted to uppercase
  3. Spaces removed
  4. Non-alphanumeric characters (except -) removed

These are equivalent:

  • abcd-efgh-ijkl-mnop
  • ABCD-EFGH-IJKL-MNOP
  • abcd efgh ijkl mnop
  • AbCdEfGhIjKlMnOp

Device Binding Behavior

The system supports multi-device binding. Each code can be used on up to max_devices different devices (default: 3).

Scenario Action
No device cookie Generate new device ID, set cookie
Device already in device_hash list Allow access
Device not in list, room available Add device to list, allow access
Device not in list, list full Reject with DEVICE_LIMIT_REACHED
shared=TRUE in sheet Skip all device checks, allow any device

Device hash format: Pipe-separated hashes: hash1|hash2|hash3

Cookie name: {COOKIE_PREFIX}_device (default prefix: gate)


GET /access-me

Checks if the current session is valid. Does not read from the sheet.

Request

GET /.netlify/functions/access-me
Cookie: gate_access=eyJhbGciOiJIUzI1NiIs...

No request body. Cookies are sent automatically by the browser.

Successful Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true,
  "label": "Acme Corp Demo",
  "expires_at": "2025-12-31T00:00:00.000Z"
}

Error Response (No Session or Invalid Token)

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "ok": false
}

Usage Pattern

Call this endpoint on app boot to determine whether to show the gate or the app:

async function checkAccess() {
  const res = await fetch('/.netlify/functions/access-me', {
    credentials: 'include'  // Important: send cookies
  });
  const data = await res.json();
  return data.ok;
}

// On app load
if (await checkAccess()) {
  showApp();
} else {
  showGate();
}

POST /access-log

Logs a usage event to the Usage sheet. Requires a valid session.

Request

POST /.netlify/functions/access-log
Content-Type: application/json
Cookie: gate_access=eyJhbGciOiJIUzI1NiIs...

{
  "event": "level_complete",
  "path": "/game/level-3",
  "meta": {
    "score": 9500,
    "time_seconds": 120
  }
}

Body Parameters:

Field Type Required Description
event string Yes Event name (e.g., open_app, level_complete)
path string No Current page/route
meta object No Arbitrary metadata (stored as JSON string)

Successful Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true
}

Error Response (No Session)

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "ok": false
}

Logged Fields

Each call appends a row to the Usage sheet:

Column Value
ts Current ISO timestamp
event From request body
label From JWT claims
code_hash From JWT claims
device_hash Hash of device cookie value
path From request body
meta_json JSON.stringify(meta)

Common Events to Log

Event When to Fire
open_app App loads after gate
page_view User navigates to a page
feature_used User interacts with a feature
level_start Game level begins
level_complete Game level ends
error An error occurs
logout User clears session (if you implement this)

Error Codes Reference

Code HTTP Status Meaning
MISSING_CODE 400 No code provided in request body
INVALID_JSON 400 Request body is not valid JSON
INVALID_CODE 401 Code hash not found in Access sheet
INVALID_ORIGIN 403 Request origin failed CSRF validation
REVOKED 403 Code has been revoked (revoked=TRUE)
EXPIRED 403 Current time is past expires_at
DEVICE_LIMIT_REACHED 403 Code already used on maximum devices
RATE_LIMITED 429 Too many requests, try again later
MISSING_EXPIRES_AT 500 Access row has no expires_at value
SERVER_CONFIG_ERROR 500 Missing or invalid server configuration
INTERNAL_ERROR 500 Unexpected server error

Rate Limiting

The /access-verify endpoint is rate-limited to prevent brute force attacks.

  • Default: 10 requests per minute per IP
  • Response: 429 Too Many Requests with Retry-After header
  • Configuration: Set RATE_LIMIT_WINDOW_MS and RATE_LIMIT_MAX_REQUESTS env vars

Cookies Reference

Cookie names use a configurable prefix (default: gate, set via COOKIE_PREFIX env var).

Cookie Purpose Attributes Max-Age
{prefix}_device Device identifier HttpOnly, Secure, SameSite=None, Path=/ 31536000 (1 year)
{prefix}_access Session JWT HttpOnly, Secure, SameSite=None, Path=/ 604800 (7 days)

Notes:

  • Default prefix is gate (e.g., gate_device, gate_access)
  • Production: SameSite=None + Secure=true (supports cross-origin requests)
  • Local dev: SameSite=Lax + Secure=false (browsers reject None without Secure)

CORS and Credentials

All endpoints support CORS for same-origin requests. When calling from JavaScript:

// Always include credentials to send cookies
fetch('/.netlify/functions/access-verify', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code: userCode })
});

Rate Limiting

Built-in rate limiting protects /access-verify against brute force:

  • Default: 2 requests per minute per IP
  • Configurable: Set RATE_LIMIT_WINDOW_MS and RATE_LIMIT_MAX_REQUESTS env vars

Important: Rate limiting is in-memory and resets on cold starts. This provides basic abuse protection but is not durable. For stronger guarantees, add Netlify Edge Functions or an external rate limiting service.