Skip to content

Cloud Function API ​

Cloud Function API is a serverless function service that allows third-party applications to invoke pre-written JavaScript functions via HTTP requests. You can create and edit cloud functions in the management panel, then call them through the API.

Overview ​

ItemDescription
Base Path/cf/api/
AuthenticationBasic (app mode) or Bearer (user mode)
Data FormatJSON
Function Quota50 per application by default; owners can request an increase up to 10,000
Code Size Limit256 KB
Timeout Range5,000 ~ 30,000 ms
Memory Limit64 MB
Return Value Limit1 MB

Authentication ​

The regular /cf/api/{folder}/{name} route is always authenticated, even when the target function has requireAuth=0. Anonymous callers must explicitly use one of these routes:

  • Wrapped JSON: /cf/api/public/{clientId}/{folder}/{name}
  • Raw HTTP, website downloads, and webhooks: /cf/http/public/{clientId}/{folder}/{name}

requireAuth=0 allows anonymous identity at an explicit public route; it never silently turns the regular authenticated URL into a public URL.

Cloud Function API supports two authentication modes via the Authorization header:

App Mode (Basic) ​

Authenticate using the application's client_id and client_secret.

Authorization: Basic base64(client_id:client_secret)

⚠️ Security Warning: The client_secret grants full administrative access to your application. Never embed it in client-side applications distributed to users. App mode should only be used in trusted server environments or internal development tools.

Recommended Architecture: User-facing clients should use Bearer mode (user OIDC login) to call cloud functions. Inside cloud functions, use caller + $permission to verify identity and permissions, and $data to read/write data. This is the most secure integration pattern.

User Mode (Bearer) ​

Authenticate using the user's OAuth access_token. The cloud function can access caller information through the caller object.

Authorization: Bearer <access_token>

Replay Protection ​

All API requests must include the following headers to prevent replay attacks:

HeaderDescription
X-TimestampCurrent Unix timestamp (seconds), must be within ±5 minutes of server time
X-NonceRandom unique string, 16-64 alphanumeric characters, different for each request
http
X-Timestamp: 1719000000
X-Nonce: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Core Concepts ​

Function Quota and Increase Requests ​

Applications can create 50 cloud functions by default. The management page and these endpoints return functionQuota with used, limit, remaining, maximumLimit, and the latest request:

http
GET /cf/manage/{clientId}/overview
GET /cf/manage/{clientId}/functions

The application owner can request an increase. An administrator may approve a value lower than the request, but it must remain above the current limit:

http
POST /oauth/apps/{clientId}/cloud-function-limit/request
Authorization: Bearer <owner_access_token>
Content-Type: application/json

{
  "requestedLimit": 100,
  "reason": "Production functions are split by download, payment, and notification domains"
}

Function creation and quota approval share an application-scoped lock so concurrent creates cannot cross the effective limit. The request uses the platform's existing service-review workflow and does not require developer-specific R2 or server configuration.

Cloud Functions ​

A cloud function is a piece of JavaScript code executed in a sandboxed environment. Each function has the following properties:

PropertyTypeDescription
namestringFunction name, unique within the application, only letters, numbers, underscores
descriptionstringFunction description
folderstring | nullFolder path for organizing functions
timeoutnumberTimeout in ms, range 5,000 ~ 30,000; payment/network functions may use 30 seconds
enablednumberWhether enabled (1 = enabled)
requireAuthnumberWhether authentication is required (1 = required, default; 0 = anonymous public access)
invokeAccessstringInvoke entry: http (default, allows HTTP/API and $invoke) or cloud_function ($invoke only)
invokeMethodstringHTTP invocation method: post (default), get, or both
outputModestringAPI endpoint response shape: default (default), direct, or mixed
requestModestringInput protocol: wrapped (default { params }) or explicit raw (third-party Webhook)
rawHttpUrlstring | nullGenerated public Raw URL returned by management APIs when requestMode=raw
configVarsobjectReadable non-sensitive configuration such as URLs, flags, and bundle IDs; accessible through env
configVarsSourcestringJSONC source for ordinary configuration; preserves //, /* */ comments and trailing commas
envVarsobjectBackward-compatible write-only function Secrets; reads return __STARECHO_SECRET_SET__ placeholders only

Use invokeAccess = cloud_function for private helper functions. In this mode, users, anonymous access, Basic/Bearer HTTP requests, and cron jobs cannot invoke the function directly; only $invoke from another cloud function can call it.

Write-only Secret Store ​

Application Secrets are shared independently of individual functions and are intended for Apple .p8 keys, API keys, webhook Secrets, and OAuth client Secrets. Management endpoints return only names, isSet, and timestamps:

http
GET    /cf/manage/{clientId}/secrets
PUT    /cf/manage/{clientId}/secrets/{NAME}   # body: { "value": "..." }
DELETE /cf/manage/{clientId}/secrets/{NAME}

The management UI accepts multiline Secret values, including pasted PEM and .p8 private keys. Input is masked by default, and line breaks are submitted and encrypted unchanged. The editor is cleared after a successful write, and management clients cannot read the plaintext back.

PUT responses are immediately redacted. Application- and function-level Secrets may use an empty string to register a Key whose value still requires manual configuration. Such entries return isSet=false; $secrets.has(NAME) is false, and $secrets.ref(NAME) reports that the Secret is not configured. The __STARECHO_SECRET_SET__ placeholder still preserves an existing function-level Secret. Delete application Secrets with the endpoint above, or pass deleteEnvVars: ['NAME'] when updating a function to explicitly delete legacy function-level Secrets. env.NAME remains available for backward compatibility, while managed signing APIs should use $secrets.ref('NAME'). Console logs, errors, and persisted execution records redact configured Secret values. HTTP and function-call results remain byte-preserving so presigned URLs, webhook signatures, ciphertext, and opaque tokens are not corrupted; function code must therefore never explicitly return a Secret value.

Key-only backup and restore ​

When cloud_function is selected for export, only the service selection is required:

http
POST /backup/manage/{clientId}/export
Content-Type: application/json

{
  "services": ["cloud_function"]
}

New backups write only application Secret Store and legacy function envVars names to cloud-function/secret-keys.json; actual values are never written to the archive. Restore creates empty Keys and returns requiredSecretConfiguration, grouped into application- and function-level lists, so the operator can configure each value manually. The management UI shows this list both before and after import.

StarEcho still imports legacy v2 cloud-function/secrets.enc.json archives. Only those old backups require their original multipart secretsPassphrase. A v1 backup has no Secret snapshot and preserves the target application's current application Secret Store.

Cron Rules ​

Cron rules periodically trigger cloud functions. Manage them through:

http
GET    /cf/manage/{clientId}/cron
POST   /cf/manage/{clientId}/cron
PUT    /cf/manage/{clientId}/cron/{ruleId}
DELETE /cf/manage/{clientId}/cron/{ruleId}

Common create/update fields:

FieldTypeDescription
namestringOptional display name for the cron rule
functionUidstringPrimary target cloud function UID
cronExprstring5-field cron expression: minute hour day month weekday
scheduleTypestringcron, workday, holiday, or festival
scheduleConfigstringVisual schedule config JSON string for multiple times, festivals, etc.
functionTargetsstringOptional JSON array of target function UIDs; empty means only functionUid
timeZonestringIANA time zone, such as Asia/Shanghai or UTC
paramsobjectJSON params merged into the cloud function params when the rule fires
maxExecutionsnumberMaximum execution count; 0 or omitted means unlimited
enablednumberFor updates: 1 enabled, 0 disabled

Cron executions run as app identity. The rule params are merged first, then system fields are overwritten:

js
{
  ...ruleParams,
  __trigger: 'cron',
  ruleId,
  cronExpr,
  scheduleType,
  timeZone
}

When the server is managed by PM2 in multi-instance or cluster mode, StarEcho uses a MySQL distributed lock plus a minute-level execution bucket. The same rule only executes once in the same minute, preventing duplicate triggers across instances.

Sandbox Environment ​

Cloud functions execute in an isolated Node.js Worker sandbox with the following characteristics:

  • Security Isolation: Each execution runs in an independent VM context, 64 MB memory limit
  • Timeout Control: Functions are terminated if they exceed the configured timeout
  • Console Capture: console.log/warn/error/info output is recorded in execution logs (up to 100 entries)
  • No File System Access: Cannot access the host file system
  • Controlled Network Access: Built-in fetch() for HTTP/HTTPS requests (internal network addresses are blocked)
  • Concurrency Limit: Up to 5 concurrent function instances per application

Function Context ​

Your function code can access the following global variables and built-in APIs:

VariableTypeDescription
paramsobjectParameters passed by the caller (read-only)
envobjectMerged configuration and Secrets (read-only; preserves existing env.KEY behavior)
callerobjectCaller information (read-only)
caller.modestring"user", "app", or "anonymous"
caller.uidstring | undefinedUser uid (only present in user mode)
consoleobjectLogging output (log, warn, error, info)
$dataobjectData collection built-in API (supports atomic operators, sorting, filtering — see below)
$permissionobjectPermission group built-in API (see below)
$accountobjectAccount built-in API (fetch time-limited user avatar URL, see below)
$storageobjectFile storage built-in API (upload, download, delete, list — see below)
$cryptoobjectDigests, HMAC, AES-GCM, and ES256 JWS signing/verification
$certificatesobjectFixed references to published, application-scoped JWS/X.509 policies
$secretsobjectOpaque Secret references; management clients cannot read values back
$appleIapobjectApp Store Server API token creation and managed transaction queries
$invokefunctionInvoke another cloud function in the same app; use withBasic for cross-app invocation (see below)
fetchfunctionHTTP requests (public addresses only)
btoa / atobfunctionBase64 encoding/decoding

Standard JavaScript built-in objects are also available: JSON, Math, Date, Promise, Map, Set, RegExp, URL, Array, Object, String, Number, Boolean, Error, etc.

The following are blocked: setTimeout, setInterval, process, require, Buffer, global, globalThis, __dirname, __filename.

Writing Code ​

⚠️ Important: Do NOT define a function main() or any entry function. Your code is automatically wrapped in an async IIFE — write your logic directly using await and return.

javascript
// ✅ Correct: Write code directly
const name = params.name || 'World'
return { message: `Hello, ${name}!` }

// ❌ Wrong: Do NOT define a main function
// async function main(params, caller) { ... }
// The function will only be defined but never called, and its parameters will shadow the globals

Return Values ​

The value of the last expression in the function is used as the return value. You can also use an explicit return statement. Return values are JSON-serialized and must not exceed 1MB.

javascript
// Example: Simple function
const greeting = params.name || 'World'
return { message: `Hello, ${greeting}!` }

Built-in APIs ​

Cross-App Access ​

By default, $data, $permission, $storage, and $invoke access resources owned by the current cloud function's application. To access another application, create a target app client with withBasic(clientId, clientSecret). The platform validates the passed Client ID and Client Secret using Basic authentication and then performs the operation with the target application's app identity.

When the target app's collection, permission, storage operation, or invoked function should run as a user, call asUser(uid) on the current client or on the client returned by withBasic. The uid is the platform-wide user ID, so it can represent the same logged-in user across apps. It only affects that bridge call and does not mutate the current function's caller.

javascript
const targetClientId = env.PARTNER_APP_ID
const targetClientSecret = env.PARTNER_APP_SECRET

const partnerData = $data.withBasic(targetClientId, targetClientSecret)
const partnerPermission = $permission.withBasic(targetClientId, targetClientSecret)
const partnerStorage = $storage.withBasic(targetClientId, targetClientSecret)
const invokePartner = $invoke.withBasic(targetClientId, targetClientSecret)
const partnerUid = params.partnerUid || caller.uid

const profile = await partnerData.asUser(partnerUid).get('profiles', partnerUid)
const perm = await partnerPermission.asUser(partnerUid).checkPermission(partnerUid, 'trade.exchange')
const file = await partnerStorage.asUser(partnerUid).getDownloadUrl('public/rules.json')
const result = await invokePartner.asUser(partnerUid)('shared_exchange_rate', { from: 'gold', to: 'diamond' })

return { profile: profile.document, allowed: perm.hasPermission, fileUrl: file.url, result }

Security note: Store the target app's client_secret in the current cloud function's environment variables. Never ship it to clients or hard-code it in function source. The target app must be approved and must have the corresponding service enabled: Data, Permission, Storage, or Cloud Function.

$data — Data Collection Bridge ​

$data allows cloud functions to directly access Data API collections without making HTTP requests. The Data service must be enabled for the application in the management panel.

All methods return Promises and support await.

MethodDescription
$data.get(collection, documentId)Get a single document
$data.set(collection, documentId, data, ownerUid?)Create or overwrite a document
$data.list(collection, options?)List documents (supports sorting and filtering)
$data.count(collection, options?)Count documents matching optional filters
$data.top(collection, indexName, options?)Read top documents by an ordered index, useful for leaderboards
$data.around(collection, indexName, documentId, options?)Read the ranking window around a document by ordered index
$data.me(collection, indexName, options?)Read the current user's ranking window by ordered index
$data.update(collection, documentId, data)Partially update a document (supports operators)
$data.delete(collection, documentId)Delete a document
$data.inc(collection, documentId, fields)Atomic increment (shortcut for $data.update with $inc)
$data.withBasic(clientId, clientSecret)Create a client for another app's Data service; returns the same method set
$data.asUser(uid)Access the current app's Data service as a specific user UID; can also be chained after withBasic() for the target app

options parameter (for list):

FieldTypeDescription
ownerUidstringFilter by owner
limitnumberItems per page (default 50, max 100)
offsetnumberOffset
sortBystringSort by a field in data (e.g. 'score')
sortOrderstringSort direction: 'asc' or 'desc' (default 'desc')
whereobjectConditional filter (see below)

ordered index options (for top / around / me):

FieldTypeDescription
limitnumberNumber of entries returned by top, default 50, max 100
offsetnumberPagination offset for top
ownerUidstringFilter top to one owner
ownerUidsstring | string[]Filter top to multiple owners, useful for friend rankings
partitionobjectFilter by ordered-index partition, such as { groupId, variant }
beforenumberEntries before the target for around / me
afternumberEntries after the target for around / me

where conditional filter:

Filter documents by fields in their data JSON. Two forms are supported:

FormSyntaxExample
Equality{ field: value }{ level: 10 } — exact match
Operator{ field: { $op: value } }{ score: { $gt: 1000 } }

Operators: $eq (equal), $ne (not equal), $gt, $gte, $lt, $lte

Multiple conditions are combined with AND logic:

javascript
// Single equality
await $data.list('players', { where: { level: 10 } })

// Multiple conditions (AND)
await $data.list('leaderboard', {
  where: {
    score: { $gt: 5000 },
    tier: 'premium',
  },
  sortBy: 'score',
  sortOrder: 'desc',
})

// Range query
await $data.list('products', {
  where: {
    price: { $gte: 100, $lte: 500 },
  },
})

// Count with filters
const { count } = await $data.count('players', {
  where: { level: { $gt: 5 } },
})

The where filter operates on the data JSON column. Fields are validated as [a-zA-Z_][a-zA-Z0-9_]{0,63} to prevent SQL injection.

Return values:

MethodReturnsDescription
$data.get(){ document: { documentId, ownerUid, data, createdAt, updatedAt } }Full document with data content
$data.list(){ documents[], total, limit, offset }Metadata only — each document has documentId, ownerUid, createdAt, updatedAt (no data)
$data.count(){ count }Number of matching documents

Note: $data.list() does not return the data field to save bandwidth and performance. Use $data.get(collection, documentId) to retrieve the full document content.

If you need to page/sort first and then read full document content, list the document IDs first and fetch each document by ID:

javascript
const page = await $data.list('leaderboard', {
  sortBy: 'score',
  sortOrder: 'desc',
  limit: 10,
})

const items = await Promise.all(page.documents.map(async ref => {
  const full = await $data.get('leaderboard', ref.documentId).catch(() => null)
  return full?.document
    ? { documentId: ref.documentId, ownerUid: ref.ownerUid, ...full.document.data }
    : null
}))

return { items: items.filter(Boolean), total: page.total }

update operators:

The data parameter of $data.update supports operator syntax for atomic updates:

OperatorDescriptionExample
$setSet field values{ $set: { name: 'new' } }
$unsetRemove fields{ $unset: { tempField: '' } }
$incNumeric increment (negative to decrement){ $inc: { score: 10, lives: -1 } }
$pushAppend element to array{ $push: { tags: 'new_tag' } }
$pullRemove matching element from array{ $pull: { tags: 'old_tag' } }

When the data parameter contains keys starting with $, operator mode is activated automatically. Operators can be combined:

javascript
await $data.update('players', 'player_001', {
  $inc: { score: 50, combo: 1 },
  $push: { achievements: 'first_win' },
  $set: { lastPlayedAt: new Date().toISOString() },
})

Shortcut: $data.inc(collection, documentId, fields) is equivalent to $data.update(collection, documentId, { $inc: fields })

Behavior when the document or field does not exist:

CallMissing documentMissing field
$data.set(collection, documentId, data, ownerUid?)Creates the documentReplaces the whole data object; missing business fields do not cause errors
$data.update(collection, documentId, { field: value })Throws not_foundAdds the top-level field
$data.update(..., { $set: { field: value } })Throws not_foundAdds the top-level field
$data.update(..., { $inc: { field: 1 } })Throws not_foundWrites 0 + 1; the $inc value must be a number
$data.update(..., { $push: { field: value } })Throws not_foundCreates an array and appends the value; non-array fields are treated as empty arrays
$data.update(..., { $pull: { field: value } })Throws not_foundMissing or non-array fields are ignored

$data.update updates top-level fields. { $set: { 'profile.name': 'A' } } writes a top-level field literally named "profile.name"; it does not traverse into profile.

Nested object updates:

For nested updates, read the document, modify the nested object in cloud-function code, then write back the changed top-level object:

javascript
function cloneJson(value) {
  return JSON.parse(JSON.stringify(value || {}))
}

function setNested(data, path, value) {
  const keys = String(path).split('.').filter(Boolean)
  if (!keys.length) throw new Error('path is required')

  let cursor = data
  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i]
    if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) {
      cursor[key] = {}
    }
    cursor = cursor[key]
  }

  cursor[keys[keys.length - 1]] = value
  return keys[0]
}

async function updateNested(collection, documentId, path, value) {
  const { document } = await $data.get(collection, documentId)
  const data = cloneJson(document.data)
  const rootKey = setNested(data, path, value)

  await $data.update(collection, documentId, {
    $set: { [rootKey]: data[rootKey] },
  })

  return { status: 'ok', documentId, path }
}

await updateNested('player_profile', caller.uid, 'settings.audio.bgmVolume', 80)

This rewrites the changed top-level object. If multiple cloud functions concurrently modify the same top-level object, for example settings.*, the later write can overwrite the earlier one. For highly concurrent data, split fields into smaller top-level objects or separate documents.

Example: Anti-Cheat Score Upload ​

javascript
// Score is calculated server-side, client cannot tamper

const { level, combo, accuracy, playerUid } = params

if (!level || !combo || accuracy === undefined || !playerUid) {
  return { error: 'missing_params' }
}

const score = level * 1000 + combo * 50 + Math.floor(accuracy * 500)

const existing = await $data.get('leaderboard', playerUid).catch(() => null)
const existingData = existing?.document?.data
if (existingData?.score >= score) {
  return { updated: false, currentBest: existingData.score }
}

await $data.set('leaderboard', playerUid, {
  score, level, combo, accuracy,
  timestamp: new Date().toISOString(),
}, playerUid)

console.log(`Player ${playerUid} new high score: ${score}`)
return { updated: true, score }

Example: User Settings ​

javascript
if (params.action === 'save') {
  await $data.set('settings', caller.uid, {
    theme: params.theme || 'default',
    language: params.language || 'zh-CN',
    notifications: params.notifications !== false,
  })
  return { saved: true }
} else {
  const doc = await $data.get('settings', caller.uid).catch(() => null)
  return doc?.document?.data || { theme: 'default', language: 'zh-CN', notifications: true }
}

Example: Atomic Operations (Counter / Favorites) ​

javascript
const { playerUid, action, itemId } = params

if (action === 'like') {
  // Atomically increment like count
  await $data.update('posts', params.postId, {
    $inc: { likes: 1 },
  })
  return { success: true }
}

if (action === 'collect') {
  // Add item to user's favorites
  await $data.update('favorites', playerUid, {
    $push: { items: itemId },
    $inc: { count: 1 },
  })
  return { success: true }
}

if (action === 'uncollect') {
  // Remove item from favorites
  await $data.update('favorites', playerUid, {
    $pull: { items: itemId },
    $inc: { count: -1 },
  })
  return { success: true }
}

Example: Leaderboard Query ​

javascript
// Get top 10 players sorted by score descending
const topPage = await $data.list('leaderboard', {
  sortBy: 'score',
  sortOrder: 'desc',
  limit: 10,
})
const top10 = await Promise.all(topPage.documents.map(async ref => {
  const full = await $data.get('leaderboard', ref.documentId).catch(() => null)
  return full?.document
    ? { documentId: ref.documentId, ...full.document.data }
    : null
}))

// Multi-field filter: find players with high score and premium tier
const elitePage = await $data.list('leaderboard', {
  where: {
    score: { $gt: 5000 },
    tier: 'premium',
  },
  sortBy: 'score',
  sortOrder: 'desc',
})
const elitePlayers = await Promise.all(elitePage.documents.map(async ref => {
  const full = await $data.get('leaderboard', ref.documentId).catch(() => null)
  return full?.document
    ? { documentId: ref.documentId, ...full.document.data }
    : null
}))

return {
  top10: top10.filter(Boolean),
  elitePlayers: elitePlayers.filter(Boolean),
}

$permission — Permission Group Bridge ​

$permission allows cloud functions to directly access Permission API roles and permission data without making HTTP requests. The Permission service must be enabled for the application in the management panel.

All methods return Promises and support await.

MethodDescription
$permission.listRoles()List all roles
$permission.getRole(name)Get role details
$permission.listRoleMembers(roleName, { page, limit })List members assigned to one role
$permission.getUserRoles(uid)Get a user's role list
$permission.getUserPermissions(uid)Get a user's merged permission list
$permission.checkPermission(uid, permission)Check if a user has a specific permission
$permission.assignRole(uid, roleName)Assign a role to a user
$permission.removeRole(uid, roleName)Remove a role from a user
$permission.updateRolePermissions(roleName, { allow, deny })Update a role's allow/deny permission lists
$permission.withBasic(clientId, clientSecret)Create a client for another app's Permission service; returns the same method set
$permission.asUser(uid)Access the current app's Permission service as a specific user UID; can also be chained after withBasic() for the target app

Cloud functions execute with app-level identity. $data and $permission have full access. Use caller info in your code for access control.

Example: VIP Auto-Upgrade ​

javascript
const { playerUid } = params

const record = await $data.get('purchase_records', playerUid).catch(() => null)
const totalSpent = record?.document?.data?.totalSpent || 0

let targetRole = null
if (totalSpent >= 10000) targetRole = 'vip_gold'
else if (totalSpent >= 5000) targetRole = 'vip_silver'
else if (totalSpent >= 1000) targetRole = 'vip_bronze'

if (!targetRole) return { upgraded: false }

const userRoles = await $permission.getUserRoles(playerUid)
const hasRole = userRoles.roles?.some(r => r.name === targetRole)
if (hasRole) return { upgraded: false, currentRole: targetRole }

const vipRoles = ['vip_bronze', 'vip_silver', 'vip_gold']
for (const old of vipRoles) {
  if (userRoles.roles?.some(r => r.name === old)) {
    await $permission.removeRole(playerUid, old)
  }
}
await $permission.assignRole(playerUid, targetRole)

console.log(`Player ${playerUid} upgraded to ${targetRole}`)
return { upgraded: true, newRole: targetRole }

Example: Permission Check Middleware ​

javascript
// Check permissions before performing an action

const { action } = params
const permMap = {
  'delete_post': 'post.delete',
  'ban_user': 'user.ban',
  'edit_config': 'config.edit',
}

const requiredPerm = permMap[action]
if (!requiredPerm) return { allowed: false, reason: 'unknown_action' }

const result = await $permission.checkPermission(caller.uid, requiredPerm)
return { allowed: result.hasPermission, permission: requiredPerm }

$account — User Avatar ​

$account exposes read-only account queries. Currently it provides a single method, getAvatar(uid), which returns a user's avatar URL.

The returned URL is signed and time-limited (aligned to the hour boundary, expires within 1 hour) and can be handed directly to clients as an <img src="...">. Calling this API inside a cloud function does not require the storage/permission services to be enabled.

getAvatar(uid) ​

javascript
const result = await $account.getAvatar(uid)

Parameters

NameTypeRequiredDescription
uidstringYesTarget user's uid

Return value

typescript
{
    uid: string         // echoed input
    exists: boolean     // whether the user exists AND has set a custom avatar; false means a default avatar was returned
    url: string         // signed absolute URL (http(s)://...), valid for up to 1 hour
    expiresAt: number   // signature expiration (Unix seconds, aligned to hour boundary)
}

Example — fetch the caller's own avatar

javascript
if (!caller.uid) return { error: 'not_logged_in' }
const me = await $account.getAvatar(caller.uid)
return { avatar: me.url, expiresAt: me.expiresAt }

Example — fetch any user's avatar by uid

javascript
const target = await $account.getAvatar(params.uid)
return {
    avatar: target.url,
    exists: target.exists,
    expiresAt: target.expiresAt,
}

Example — batch fetch (concurrent)

javascript
const uids = params.uids || []
const results = await Promise.all(uids.map(uid => $account.getAvatar(uid)))
return { avatars: results }

Tip: expiresAt is the signature expiration aligned to the hour boundary so clients/CDNs can cache the URL. If you need a longer-lived link, simply call the cloud function again from the client as expiration approaches.

$storage — File Storage Bridge ​

$storage allows cloud functions to directly access the Storage API for file operations without manually handling authentication, replay protection, or HTTP calls. The Storage service must be enabled for the application in the management panel.

All methods return Promises and support await.

MethodDescription
$storage.getCapabilities() / getUsage()Get effective limits, multipart capabilities, quota, reservations, and monthly URL-issuance diagnostics; actual transfer is explicitly null when unavailable
$storage.getUploadUrl(path, size, contentType?, options?)Get a single-upload URL and one-time token; old calls remain compatible
$storage.confirmUpload(path, token)Confirm upload with token, record metadata, update quota
$storage.createMultipartUpload(path, options)Create a resumable multipart session
$storage.getMultipartUploadUrls(path, uploadId, partNumbers)Batch issue part PUT URLs
$storage.listMultipartUploadParts(path, uploadId)List provider-received parts
$storage.completeMultipartUpload(path, uploadId, options)Validate parts and complete idempotently
$storage.abortMultipartUpload(path, uploadId)Abort an unfinished session
$storage.listMultipartUploads(options?)List multipart sessions
$storage.getDownloadUrl(path, options?)Get GET and HEAD URLs with response overrides
$storage.head(path)Read authoritative object-store metadata
$storage.delete(path)Delete a file
$storage.list(options?)List files (supports prefix and pagination)
$storage.getMeta(path)Get file metadata
$storage.withBasic(clientId, clientSecret)Create a client for another app's Storage service; returns the same method set
$storage.asUser(uid)Access the current app's Storage service as a specific user UID; can also be chained after withBasic() for the target app

Method parameters:

MethodParameterTypeRequiredDescription
getUploadUrlpathstring✅File path, 1–512 characters
sizenumber✅Final PUT body byte count; default limit 1 GiB, query getCapabilities()
contentTypestring❌MIME type, defaults to application/octet-stream
optionsobject❌sha256, metadata, allowOverwrite, expiresIn
confirmUploadpathstring✅File path (same as upload)
tokenstring✅Upload token returned by getUploadUrl
getDownloadUrlpathstring✅File path
optionsobject❌expiresIn, downloadName, contentType, cacheControl
deletepathstring✅File path
listoptions.prefixstring❌Path prefix filter
options.limitnumber❌Items per page, default 50, max 200
options.pagenumber❌Page number, default 1
getMetapathstring✅File path

Return values:

getUploadUrl returns { url, token, expiresIn, fullPath, requestedSize, contentType, contentLengthBound, requiredHeaders }:

FieldTypeDescription
urlstringPresigned PUT URL (1 hour by default; use expiresIn)
tokenstringOne-time upload token (64-char hex), required for confirmUpload
expiresInnumberURL lifetime (seconds)
fullPathstringFull path inside the storage bucket
requestedSizenumberExpected byte count submitted when requesting the URL
contentTypestringMIME type used when requesting the URL
contentLengthBoundbooleanWhether the signature binds the exact HTTP body length
requiredHeadersobjectExact length and MIME request headers required when binding is enabled

getDownloadUrl returns { url, headUrl, expiresIn, size, contentType, etag, sha256, sha256Verification, sha256VerifyStatus }. The GET URL supports Range; use the separate headUrl for HEAD because SigV4 binds the HTTP method. Default-R2 objects are promoted from declared to platform_verified only after the background worker streams and hashes the full object.

FieldTypeDescription
urlstringPresigned GET URL (valid for 1 hour)
headUrlstringConcurrently issued HEAD-only URL; it is not interchangeable with the GET URL
expiresInnumberURL lifetime (seconds)
sizenumberFile size (bytes)
contentTypestringMIME type

list returns { total, files } where each item in files includes path, size, contentType, uploadedBy, createdAt, updatedAt.

getMeta returns { path, size, contentType, uploadedBy, createdAt, updatedAt }.

delete returns { status: 'ok' }.

⚠️ Note: $storage.getUploadUrl() returns a presigned URL and one-time token; it does not upload the file. New calls may pass a fourth { bindContentLength: true } argument to bind the exact byte count; omitting it preserves legacy compatibility. Use the URL unchanged. After PUT succeeds, call $storage.confirmUpload(); StarEcho reads the authoritative size and type with HeadObject before updating metadata and quota. Since $storage.confirmUpload(path, token) does not pass size/contentType, the actual object-store values are adopted, so cloud-function and legacy callers do not need to match a declared size exactly. Multipart is recommended above 100 MiB; see the Storage API for the complete flow.

Example: Complete Upload Flow ​

The cloud function receives file content from the client, uploads it, and confirms:

javascript
const { fileName, fileData } = params
// fileData: base64-encoded file content

// 1. Build the final binary body; byteLength is the upload size
const binaryData = Uint8Array.from(atob(fileData), c => c.charCodeAt(0))

// 2. Get upload URL and token
const uploadRes = await $storage.getUploadUrl(
  `uploads/${fileName}`,
  binaryData.byteLength,
  params.contentType,
  { bindContentLength: true },
)
const { url, token } = uploadRes

// 3. PUT the file to the presigned URL (cloud function fetch can reach public addresses)
await fetch(url, {
  method: 'PUT',
  headers: { 'Content-Type': params.contentType || 'application/octet-stream' },
  body: binaryData.buffer,
})

// 4. Confirm upload with token
const result = await $storage.confirmUpload(`uploads/${fileName}`, token)

return { uploaded: true, path: result.path, size: result.size }

Example: File Download & Distribution ​

javascript
// Get a download link to hand to the client
const { url, expiresIn, size, contentType } = await $storage.getDownloadUrl(params.filePath)
return { downloadUrl: url, expiresIn, size, contentType }
javascript
// List all files under a user-specific prefix
const { total, files } = await $storage.list({
  prefix: `users/${caller.uid}/`,
  limit: 20,
  page: 1,
})

// Each file contains: path, size, contentType, uploadedBy, createdAt, updatedAt
return {
  total,
  files: files.map(f => ({
    path: f.path,
    size: f.size,
    contentType: f.contentType,
    createdAt: f.createdAt,
  })),
}

Example: Clean Up Expired Files ​

javascript
const { files } = await $storage.list({ prefix: 'temp/', limit: 200 })
const now = Date.now() / 1000
const expiredFiles = files.filter(f => f.createdAt && now - Number(f.createdAt) > 86400 * 7)

for (const file of expiredFiles) {
  await $storage.delete(file.path)
  console.log(`Deleted expired file: ${file.path}`)
}

return { cleaned: expiredFiles.length }

Example: User Avatar Storage ​

javascript
// Receive an avatar upload from a client and store it in the user's own directory
if (!caller.uid) return { error: 'not_logged_in' }

const { imageData, contentType } = params
const path = `avatars/${caller.uid}/${Date.now()}.png`

const uploadRes = await $storage.getUploadUrl(path, params.imageSize, contentType)
const { url, token } = uploadRes
const bytes = Uint8Array.from(atob(imageData), c => c.charCodeAt(0))
await fetch(url, { method: 'PUT', body: bytes.buffer })
const result = await $storage.confirmUpload(path, token)

return { path: result.path, size: result.size }

$crypto, $certificates, $secrets, and $appleIap ​

Existing $crypto.hash, $crypto.hmac, and $crypto.timingSafeEqual behavior is unchanged. ES256 signing remains synchronous:

javascript
const jwt = $crypto.jwsSign({
  algorithm: 'ES256',
  key: $secrets.ref('PARTNER_SIGNING_PRIVATE_KEY'),
  header: { kid: env.PARTNER_SIGNING_KEY_ID, typ: 'JWT' },
  payload,
})

For JWS values carrying an x5c chain, use a fixed version of an application-owned, published JWS/X.509 policy. The policy defines its certificate bundle, chain constraints, validation time, expected claims, and revocation behavior. The runtime does not select a hidden trust store by provider name:

javascript
const verified = await $crypto.jwsVerify({
  signedPayload,
  algorithm: 'ES256',
  policy: $certificates.policy('partner_webhook_jws_v1', 1),
})

Policy verification can perform OCSP requests and therefore always requires await. Policy names and versions must appear directly in a literal $certificates.policy('name', version) call so the runtime loads only explicitly pinned resources; do not first place them in variables or read them from environment/request values. The old trustStore, expected, and validateAt options have been removed with no compatibility fallback. A direct key: $secrets.ref('PUBLIC_KEY') remains available for fixed-public-key signature verification, but it does not validate an x5c chain, policy claims, or revocation state.

Verification failures expose code, retryable, stage, and category. Return 503 for retryable === true dependency failures so the sender can retry, and 400 for permanent signature, certificate, claim, or configuration failures. Explicit OCSP revoked and unknown statuses are permanent rejections. Do not classify failures by parsing error messages.

Application owners manage immutable, versioned certificate resources with these authenticated Cloud Function management routes:

  • GET/POST /cf/manage/{clientId}/certificate-bundles
  • GET/PUT/DELETE /cf/manage/{clientId}/certificate-bundles/{bundleId}
  • POST /cf/manage/{clientId}/certificate-bundles/{bundleId}/publish or /deprecate
  • GET/POST /cf/manage/{clientId}/jws-x509-policies
  • GET/PUT/DELETE /cf/manage/{clientId}/jws-x509-policies/{policyId}
  • POST /cf/manage/{clientId}/jws-x509-policies/{policyId}/publish or /deprecate

Application owners and platform administrators can write. Collaborators with Cloud Function view access can only read. Every create, update, publish, deprecate, and delete is written to the application audit log; certificate bundle audits contain SHA-256 fingerprints, not complete PEMs.

Create and publish a certificate bundle first, then create a policy whose certificateBundleId references the returned bundle.id. The platform allocates versions. Published definitions cannot be edited in place; publish a new version, update the literal version in the cloud function, validate it, and only then deprecate the old version. A provider-neutral policy body can look like this:

json
{
  "name": "partner_webhook_jws_v1",
  "certificateBundleId": "<published-bundle-uuid>",
  "definition": {
    "allowedAlgorithms": ["ES256"],
    "minChainLength": 2,
    "maxChainLength": 4,
    "leaf": { "ca": false, "keyTypes": ["ec"] },
    "validationTime": {
      "mode": "payloadClaim",
      "path": "signedAt",
      "unit": "milliseconds",
      "maxFutureSkewMs": 300000
    },
    "revocation": {
      "mode": "ocsp",
      "targets": "leaf",
      "unavailable": "retryableError"
    },
    "expectedClaims": [
      { "path": "audience", "equals": "my-application", "required": true }
    ]
  }
}

expectedClaims uses strict type matching and supports equals, oneOf, presence/absence checks, and conditional requiredWhen rules. Put trusted application identities, environments, and allowed business identifiers into the published policy instead of accepting them with each request.

The managed transaction API performs the documented Production-to-Sandbox fallback only for Apple error 4040010, applies a per-request timeout and limited GET retries, verifies the returned JWS, and returns an explicit environment:

javascript
const result = await $appleIap.getTransactionInfo({
  transactionId: params.transactionId,
  issuerIdSecret: 'APPLE_IAP_ISSUER_ID',
  keyIdSecret: 'APPLE_IAP_KEY_ID',
  privateKeySecret: 'APPLE_IAP_PRIVATE_KEY_P8',
  bundleId: env.APPLE_IAP_BUNDLE_ID,
  policy: $certificates.policy('apple_iap_transaction_v1', 1),
  environment: 'auto',
  requestTimeoutMs: 8000,
  retries: 1,
})

The template prefers the APPLE_IAP_PRIVATE_KEY_P8 Secret name and also accepts the existing APPLE_IAP_PRIVATE_KEY name. privateKeySecret may reference any valid Secret name.

App Store Server Notifications V2 does not require a provider-specific verification branch. Verify the outer notification, nested transaction, and nested renewal separately with explicit, fixed policy references:

javascript
const outer = await $crypto.jwsVerify({
  signedPayload: request.body.signedPayload,
  algorithm: 'ES256',
  policy: $certificates.policy('apple_iap_notification_v1', 1),
})

const data = outer.payload.data || {}
const transaction = data.signedTransactionInfo
  ? (await $crypto.jwsVerify({
      signedPayload: data.signedTransactionInfo,
      algorithm: 'ES256',
      policy: $certificates.policy('apple_iap_transaction_v1', 1),
    })).payload
  : null
const renewal = data.signedRenewalInfo
  ? (await $crypto.jwsVerify({
      signedPayload: data.signedRenewalInfo,
      algorithm: 'ES256',
      policy: $certificates.policy('apple_iap_renewal_v1', 1),
    })).payload
  : null

Production requires APPLE_IAP_APP_APPLE_ID to be a positive integer and to strictly equal the verified numeric data.appAppleId; Sandbox may omit it. Bundle ID, allowed products, environment, and App Apple ID should also be static expectedClaims in the corresponding published policies. Return 503 when any layer fails with error.retryable === true, or 400 for a permanent failure, preserving code, retryable, stage, and category in the response. The “Apple IAP Server Verification” cloud-function template demonstrates idempotent event storage with $data.transaction and createOnce without persisting complete JWS values.

$invoke — Inter-Function Invocation ​

$invoke allows a cloud function to synchronously call another cloud function within the same app and receive its return value. Use withBasic to invoke cloud functions in another app; use asUser(uid) to make the invoked function receive caller.mode === 'user' and the provided caller.uid.

javascript
const result = await $invoke(functionNameOrPath, params?, folder?)
ParameterTypeDescription
functionNameOrPathstringTarget function name, or a folder/functionName path
paramsobjectParameters to pass (optional)
folderstringTarget function's folder path (optional); omit it when the first argument already contains the path

Return value: The target function's return value directly.

Cross-app invocation:

javascript
const invokePartner = $invoke.withBasic(env.PARTNER_APP_ID, env.PARTNER_APP_SECRET)
const result = await invokePartner.asUser(caller.uid)('get_exchange_rate', { from: 'gold', to: 'diamond' }, 'shared')
// Equivalent:
// const result = await invokePartner('shared/get_exchange_rate', { from: 'gold', to: 'diamond' })

Limits:

  • Up to 10 invocations per function execution
  • Max nesting depth: 5 levels (function A → B → C → D → E)
  • By default, the target function must be enabled and within the same app
  • For cross-app calls, use $invoke.withBasic(clientId, clientSecret); the target app must be approved and have Cloud Function enabled

Example: Reuse shared logic ​

javascript
// Function: create_order
const { items, buyerUid } = params

// Call shared validation function
const validation = await $invoke('validate_items', { items })
if (!validation.valid) return { error: validation.reason }

// Call pricing function
const pricing = await $invoke('calculate_price', { items }, 'utils')
// Or:
// const pricing = await $invoke('utils/calculate_price', { items })

return { orderId: 'xxx', total: pricing.total }

Example: Cross-App Invocation ​

To call a cloud function in another application, use $invoke.withBasic() with the target app's Client ID and Client Secret. The platform validates the credentials using Basic authentication.

javascript
const targetClientId = env.PARTNER_APP_ID
const targetClientSecret = env.PARTNER_APP_SECRET
const invokePartner = $invoke.withBasic(targetClientId, targetClientSecret)

const result = await invokePartner('get_exchange_rate', {
  from: 'gold',
  to: 'diamond',
}, 'shared')
// Or invokePartner('shared/get_exchange_rate', { from: 'gold', to: 'diamond' })

const rate = result?.rate || 1
return { exchangeRate: rate }

Tip: You can still call the external Cloud Function API with fetch(), but the built-in $invoke.withBasic() path does not require manually building Authorization, X-Timestamp, or X-Nonce headers.

fetch — HTTP Requests ​

Cloud functions can use fetch() to make HTTP/HTTPS requests to public addresses.

Restrictions:

  • Only http:// and https:// protocols allowed
  • Internal network addresses blocked (localhost, 127.0.0.1, 10.*, 192.168.*, 172.16-31.*, IPv6 loopback/link-local/ULA, etc.)
  • timeoutMs is configurable from 1–15 seconds (default 10 seconds)
  • Only idempotent GET requests accept retries: 0–2; other methods are never retried automatically
  • Response body limit: 1 MB

Return value:

FieldTypeDescription
okbooleanWhether status code is 2xx
statusnumberHTTP status code
textstringResponse text
jsonanyAuto-parsed JSON (undefined if parsing fails)
headersobjectResponse headers
javascript
const resp = await fetch('https://api.example.com/data', {
  timeoutMs: 8000,
  retries: 1,
})

if (!resp.ok) return { error: `API returned ${resp.status}` }
return resp.json

API Endpoints ​

Invoke Cloud Function ​

Supports nested folder paths:

http
POST /cf/api/{functionName}
POST /cf/api/{folder}/{functionName}
GET  /cf/api/{functionName}
GET  /cf/api/{folder}/{functionName}
Content-Type: application/json

Only functions with invokeAccess = http can be invoked through HTTP/API. Whether GET, POST, or both are accepted is controlled by the function's invokeMethod setting. The default is post.

Path Parameters:

ParameterTypeDescription
functionNamestringFunction name
folderstringOptional, folder path

Required Headers:

HeaderDescription
AuthorizationAuth credentials (Basic / Bearer)
X-TimestampUnix timestamp (seconds)
X-Nonce16-64 character random string

POST Request Body:

json
{
  "params": {
    "key": "value"
  }
}

params is optional and defaults to an empty object {}.

GET Query Parameters:

  • ?params=<JSON object>: for example ?params=%7B%22platform%22%3A%22win64%22%7D
  • If params is omitted, regular query parameters are passed as params. For example, ?platform=win64 becomes { "platform": "win64" }

Success Response:

json
{
  "result": {
    "message": "Hello, World!"
  },
  "logs": ["[LOG] Processing request..."],
  "durationMs": 12
}

Timeout Response (HTTP 400):

json
{
  "error": "timeout",
  "error_description": "Function execution timed out after 5000ms",
  "logs": [],
  "durationMs": 5000
}

Execution Error Response (HTTP 400):

json
{
  "error": "execution_error",
  "error_description": "ReferenceError: foo is not defined",
  "logs": ["[LOG] Starting..."],
  "durationMs": 3
}

Anonymous Invocation (Public Endpoint) ​

When a cloud function's requireAuth is set to 0, it can be invoked via the public endpoint with no authentication. This endpoint is intended for public-facing use cases that don't need to identify the caller (e.g. version check, public announcements, stateless calculations).

http
POST /cf/api/public/{clientId}/{functionName}
POST /cf/api/public/{clientId}/{folder}/{functionName}
GET  /cf/api/public/{clientId}/{functionName}
GET  /cf/api/public/{clientId}/{folder}/{functionName}
Content-Type: application/json

The public GET endpoint can be opened directly in a browser address bar, which is useful for read-only functions such as version checks and public announcements.

Path Parameters:

ParameterTypeDescription
clientIdstringThe application's client_id (safe to expose; client_secret is NOT used)
functionNamestringFunction name
folderstringOptional folder path

⚠️ Notes:

  • Public endpoints do not require Authorization, X-Timestamp, or X-Nonce headers.
  • Inside the function, caller.mode is "anonymous" and caller.uid is undefined.
  • Only functions with requireAuth=0 can be invoked via this endpoint. If the function requires auth (requireAuth=1, the default), this endpoint returns 403 auth_required.
  • Only functions with invokeAccess=http can be invoked via this endpoint. invokeAccess=cloud_function returns 403 invoke_access_denied.
  • invokeMethod is still enforced; for example, a function configured as get cannot be invoked with POST.
  • Do NOT perform sensitive per-user operations (writing user data, deducting balance, granting rewards, etc.) inside anonymous functions.

API Endpoint Response Shape ​

Cloud functions can choose an outputMode in the function settings. This affects authenticated /cf/api/... and anonymous /cf/api/public/... response shapes. Raw HTTP endpoints /cf/http/public/... use custom HTTP response rules and are not affected by outputMode.

Assume the function returns:

javascript
return { state: 'Hello', version: 'Dev 0.45' }
outputModeResponse
default{"result":{"state":"Hello","version":"Dev 0.45"},"logs":[],"durationMs":28}
direct{"state":"Hello","version":"Dev 0.45"}
mixed{"logs":[],"durationMs":28,"state":"Hello","version":"Dev 0.45"}

mixed only merges when the function returns a non-array object. If the function return value contains logs or durationMs, the function return value wins. Strings, numbers, arrays, and other non-object values fall back to the default wrapped shape.

Request parameters and response: GET uses query parameters, while POST uses the request body. The response shape is controlled by outputMode.

Example (direct browser access):

text
https://api.starecho.net/cf/api/public/abc123/version_check

Example (UE5 / direct client POST call):

bash
curl -X POST https://api.starecho.net/cf/api/public/abc123/version_check \
  -H "Content-Type: application/json" \
  -d '{"params":{"platform":"win64"}}'

This endpoint is designed for scenarios where it is inconvenient or unsafe to ship the application secret or a user token with the client, such as game client update checks and pre-login announcement queries.

Raw HTTP Public Functions (Third-Party Webhooks) ​

Use Raw HTTP for third-party Webhooks that do not send StarEcho's { "params": ... } envelope and need exact request bytes or real HTTP response status codes:

http
POST /cf/http/public/{clientId}/{functionName}
POST /cf/http/public/{clientId}/{folder}/{functionName}
GET  /cf/http/public/{clientId}/{functionName}
GET  /cf/http/public/{clientId}/{folder}/{functionName}

The endpoint is available only when the function explicitly has requestMode="raw", requireAuth=0, and invokeAccess="http"; invokeMethod and the normal enabled/service/rate/ timeout checks still apply. Management API responses and VS Code extension/MCP tools can create, read, and update requestMode, and return the ready-to-use rawHttpUrl. Wrapped functions receive 403 raw_http_disabled on the Raw endpoint.

The function receives this request context:

FieldDescription
methodActual GET or POST method
headersHeaders with lowercase keys and preserved values
queryParsed query parameters
rawBodyUTF-8 view of the exact body captured before parsing
rawBodyBase64Base64 of the exact bytes; use this for binary protocols
rawBodyLengthExact byte length
bodyParsed JSON, text, or null
ipSource IP, preferring trusted proxy/Cloudflare headers
pathRequest path without query
urlActual URL including query

The platform never reconstructs rawBody with JSON.stringify(request.body). Raw bodies have a 1 MiB limit; the Fastify route returns a real HTTP 413 when exceeded.

javascript
const authorization = String(request.headers?.authorization || '')
const signature = authorization.match(/^Signature\s+([a-f0-9]+)$/i)?.[1] || ''
const expected = $crypto.hash('sha1', request.rawBody + env.WEBHOOK_SECRET, 'hex')

if (!$crypto.timingSafeEqual(signature, expected)) {
  return { statusCode: 400, body: { error: 'invalid_signature' } }
}

return { statusCode: 204, body: null }

The built-in “Generic JWS/X.509 Policy Verification”, “Apple IAP Server Verification”, and “Xsolla Raw Webhook” templates select compatible Raw, anonymous HTTP, POST-only defaults. The Apple template separately awaits fixed published policies for the outer notification, transaction, and renewal JWS, returns 503 for retryable dependency failures or 400 for permanent failures, and only then stores notificationUUID idempotently. The Xsolla template verifies Authorization: Signature <hex> against SHA-1 of the exact body plus its Secret and returns real 204/400 statuses.

Deployment: A database administrator should separately run scripts/database/20260725-add-cloud-function-secrets.sql from the source repository. Database migrations are excluded from dist/ and production deployment archives. Set SITE_API_URL=https://api.starecho.net in production so management APIs return a public rawHttpUrl. Cloudflare rules must cover /cf/http/; the provided nginx configuration already proxies it through location ^~ /cf/.

A Raw function can return { statusCode, headers, body }; the gateway applies all three to the actual HTTP response. Other return values use status 200. outputMode does not affect Raw responses. Runtime failures return real 500 responses and timeouts return real 504 responses, while existing /cf/api/... protocols and response shapes remain unchanged.

Error Responses ​

All error responses follow a unified format:

json
{
  "error": "error_code",
  "error_description": "Human-readable error message"
}

Common Error Codes:

Error CodeHTTP StatusDescription
unauthorized401Missing Authorization header
invalid_client401Invalid Basic credentials
invalid_token401Invalid Bearer token
replay_detected403Request replay or expired timestamp
service_not_enabled403Cloud Function service not enabled for this application
invoke_access_denied403Function is configured for internal $invoke calls only
raw_http_disabled403Function has not explicitly enabled requestMode=raw
auth_required403Function does not allow anonymous invocation (requireAuth=1)
method_not_allowed405The HTTP method is not allowed by the function's invokeMethod setting
disabled403Function is disabled
not_found404Function not found
payload_too_large413Raw HTTP body exceeds 1 MiB
timeout400 / 504400 on legacy endpoints; real 504 on Raw HTTP
execution_error400 / 500400 on legacy endpoints; real 500 on Raw HTTP

Complete Examples ​

Using Basic Authentication ​

javascript
const res = await fetch(`${API_BASE}/cf/api/calculate_score`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
    'X-Timestamp': Math.floor(Date.now() / 1000).toString(),
    'X-Nonce': crypto.randomUUID().replace(/-/g, ''),
  },
  body: JSON.stringify({
    params: { level: 5, combo: 120, accuracy: 0.95 }
  }),
})
const data = await res.json()
console.log(data.result.score)

Using Environment Variables ​

Configure ordinary values such as URLs, environment names, and feature flags in the management panel. Put API keys in application- or function-level Secrets.

The configuration editor expects a JSONC object, not .env-style KEY=value lines. // and /* */ comments and trailing commas are supported and preserved across saves and backup restores. Object keys become properties on the function's env object:

Before deploying this capability, run database_migrations/20260731-expand-cloud-function-config-vars.sql to expand encrypted JSONC source storage to MEDIUMTEXT. The migration does not modify existing configuration values.

jsonc
{
  // Disable the test payment entry before production release
  "ENABLE_TEST_PAYMENT": false,
  "WEBHOOK_URL": "https://example.com/hook",
}

Read values with env.KEY:

javascript
const message = `Player ${caller.uid} completed level ${params.level}`
console.log(`Sending notification: ${message}`)
return {
  notified: true,
  webhook: env.WEBHOOK_URL,
  message
}

Caller Identity Logic ​

javascript
if (caller.mode === 'user') {
  return { greeting: `Welcome, player ${caller.uid}!` }
} else {
  return { greeting: 'Server-to-server call' }
}

This is the recommended cloud function calling pattern for game clients distributed to users. The client uses Bearer mode (user access_token) to call cloud functions — no need to embed client_secret in the client.

Developers do not need to verify the access_token themselves. The platform automatically verifies the JWT signature and expiration before the cloud function executes. If the token is invalid or expired, the request is intercepted and returns 401 invalid_token — the cloud function code never runs. Developers only need to handle business-level permission checks.

Client-side call:

javascript
// Client code (game/frontend) — uses the user's access_token
const res = await fetch(`${API_BASE}/cf/api/submit_score`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken}`,  // token obtained after user login
    'X-Timestamp': Math.floor(Date.now() / 1000).toString(),
    'X-Nonce': crypto.randomUUID().replace(/-/g, ''),
  },
  body: JSON.stringify({
    params: { level: 5, score: 9800 }
  }),
})

Cloud function code:

javascript
// The caller object is automatically injected by the platform, token already verified
//   caller.mode = "user"
//   caller.uid  = "user uid"
//   caller.clientId = "app ID"

// ✅ Only check business permissions — no need to verify the token
const { allowed } = await $permission.checkPermission(caller.uid, 'leaderboard.submit')
if (!allowed) {
  return { error: 'no_permission', message: 'No leaderboard submit permission' }
}

// Business logic: store the score
await $data.set('leaderboard', caller.uid, {
  level: params.level,
  score: params.score,
  submittedAt: new Date().toISOString(),
}, caller.uid)

return { success: true, message: 'Score submitted' }

Combined $data + $permission Example ​

javascript
// Example: Inventory grant system with permission checking

const { adminUid, targetUid, itemId, quantity } = params

const check = await $permission.checkPermission(adminUid, 'inventory.grant')
if (!check.hasPermission) {
  return { error: 'Insufficient permissions', required: 'inventory.grant' }
}

const bag = await $data.get('inventory', targetUid).catch(() => null)
const items = bag?.document?.data?.items || {}
items[itemId] = (items[itemId] || 0) + quantity

await $data.set('inventory', targetUid, {
  items, lastModified: new Date().toISOString()
}, targetUid)

console.log(`Admin ${adminUid} granted ${quantity}x ${itemId} to ${targetUid}`)
return { success: true, itemId, quantity, newTotal: items[itemId] }

© 2026 StarEcho