云函数内置 API
本页集中整理 Cloud Function 沙盒中可用的内置 API。主 Cloud Function API 页面保留鉴权、调用接口和错误响应;这里按对象查方法、参数、返回值和示例。
速查
| 对象 | 适合场景 | 常用方法 |
|---|---|---|
$data | 读写 Data API 数据合集 | get、set、list、count、top、around、me、update、delete、inc、createOnce、incAtomic、transaction |
$permission | 查询和维护角色、权限 | listRoles、getUserRoles、checkPermission、assignRole、removeRole |
$account | 获取账号只读信息 | getUser、findUser、getAvatar |
$storage | 上传、下载、列出和删除文件 | getUploadUrl、confirmUpload、getDownloadUrl、list、delete |
$crypto | 加密解密、摘要、HMAC、ES256 JWS | encrypt、decrypt、hash、hmac、jwsSign、jwsVerify |
$certificates | 固定引用应用级 JWS/X.509 发布策略 | policy、hasPolicy |
$secrets | 只写 Secret Store 的不透明引用 | ref、has、names |
$appleIap | Apple 服务端 API Token、交易查询与通知薄封装 | createApiToken、getTransactionInfo、verifyNotification |
$invoke | 云函数之间同步调用 | $invoke(functionNameOrPath, params?, folder?) |
fetch | 调用公网 HTTP/HTTPS 服务 | fetch(url, options) |
跨应用访问统一使用 withBasic(clientId, clientSecret)。目标应用的 client_secret 应放在当前云函数的环境变量中;需要以同一个登录用户身份访问目标应用时,再链式调用 asUser(uid)。
跨应用访问
默认情况下,$data、$permission、$storage 和 $invoke 都访问当前云函数所属应用的资源。需要访问其他应用时,可以使用 withBasic(clientId, clientSecret) 创建目标应用客户端。平台会使用传入的 Client ID 和 Client Secret 做 Basic 认证,认证通过后以目标应用的应用身份执行操作。
如果目标应用的合集、权限、存储或被调用云函数需要按用户身份执行,可以在当前应用或 withBasic 返回的客户端上调用 asUser(uid)。这里的 uid 是平台全局用户 ID,适合跨应用访问“同一个用户”的数据;它只影响这一次桥接调用,不会修改当前云函数里的 caller。
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 }安全提示:目标应用的
client_secret应放在当前云函数的环境变量中,不能写入客户端或硬编码在函数源码里。目标应用也必须已审核通过,并开通对应服务(Data、Permission、Storage 或 Cloud Function)。
$data — 数据合集桥接
$data 允许云函数直接访问 Data API 的数据合集,无需发起 HTTP 请求。使用前需要在管理面板中为应用开启 Data 服务。
所有方法返回 Promise,支持 await。
| 方法 | 说明 |
|---|---|
$data.get(collection, documentId) | 获取单个文档 |
$data.set(collection, documentId, data, ownerUid?) | 创建或覆盖文档 |
$data.list(collection, options?) | 列出文档(支持排序和条件过滤) |
$data.count(collection, options?) | 统计文档数量(支持条件过滤) |
$data.top(collection, indexName, options?) | 按有序索引读取前排文档,适合排行榜 |
$data.around(collection, indexName, documentId, options?) | 按有序索引读取指定文档所在的排名窗口 |
$data.me(collection, indexName, options?) | 按有序索引读取当前用户所在的排名窗口 |
$data.update(collection, documentId, data) | 部分更新文档(支持操作符) |
$data.delete(collection, documentId) | 删除文档 |
$data.inc(collection, documentId, fields) | 原子递增($data.update 的 $inc 快捷方式) |
$data.createOnce(collection, documentId, data, ownerUid?) | 幂等创建文档;文档已存在时不覆盖,返回 created:false |
$data.incAtomic(collection, documentId, fields, options?) | 在数据库事务内锁定文档并递增数值字段,支持点路径嵌套递增 |
$data.transaction(async tx => { ... }) | 执行一组事务操作;事务客户端支持 get、set、update、delete、inc、createOnce 和 incAtomic |
$data.withBasic(clientId, clientSecret) | 创建访问其他应用 Data 服务的客户端,返回同样的方法集合 |
$data.asUser(uid) | 以指定用户 UID 访问当前应用 Data 服务;也可链在 withBasic() 返回的客户端后访问目标应用 |
options 参数(用于 list 和 count):
| 字段 | 类型 | 说明 |
|---|---|---|
ownerUid | string | 按所有者筛选 |
limit | number | 每页条数(默认 50,最大 100) |
offset | number | 偏移量 |
sortBy | string | 按 data 中的字段排序(如 'score') |
sortOrder | string | 排序方向:'asc' 或 'desc'(默认 'desc') |
where | object | 条件过滤(见下文) |
有序索引 options(用于 top / around / me):
| 字段 | 类型 | 说明 |
|---|---|---|
limit | number | top 返回条数,默认 50,最大 100 |
offset | number | top 分页偏移 |
ownerUid | string | top 按单个 owner 筛选 |
ownerUids | string | string[] | top 按多个 owner 筛选,适合好友榜 |
partition | object | 按索引分区筛选,例如 { groupId, variant } |
before | number | around / me 返回目标前多少名 |
after | number | around / me 返回目标后多少名 |
where 条件过滤:
对文档 data JSON 中的字段进行过滤。支持两种形式:
| 形式 | 语法 | 示例 |
|---|---|---|
| 等值匹配 | { field: value } | { level: 10 } — 精确匹配 |
| 操作符 | { field: { $op: value } } | { score: { $gt: 1000 } } — 大于 |
操作符: $eq(等于)、$ne(不等于)、$gt(大于)、$gte(大于等于)、$lt(小于)、$lte(小于等于)
多个条件以 AND 逻辑组合:
// 单一等值匹配
await $data.list('players', { where: { level: 10 } })
// 多条件组合 (AND)
await $data.list('leaderboard', {
where: {
score: { $gt: 5000 },
tier: 'premium',
},
sortBy: 'score',
sortOrder: 'desc',
})
// 范围查询
await $data.list('products', {
where: {
price: { $gte: 100, $lte: 500 },
},
})
// 带条件的统计
const { count } = await $data.count('players', {
where: { level: { $gt: 5 } },
})where 过滤对
dataJSON 列操作。字段名限制为[a-zA-Z_][a-zA-Z0-9_]{0,63}以防止 SQL 注入。
返回值:
| 方法 | 返回内容 | 说明 |
|---|---|---|
$data.get() | { document: { documentId, ownerUid, data, createdAt, updatedAt } } | 完整文档(含 data 内容) |
$data.list() | { documents[], total, limit, offset } | 仅元数据 — 每个文档包含 documentId、ownerUid、createdAt、updatedAt(不含 data) |
$data.count() | { count } | 匹配文档的数量 |
注意:
$data.list()不会返回data字段以节省带宽和性能。请使用$data.get(collection, documentId)获取文档的完整内容。
如果你需要先分页/排序,再读取文档内容,可以先用 $data.list() 拿到 documentId,再逐条 $data.get():
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 操作符:
$data.update 的 data 参数支持操作符语法,实现原子更新:
| 操作符 | 说明 | 示例 |
|---|---|---|
$set | 设置字段值 | { $set: { name: 'new' } } |
$unset | 删除字段 | { $unset: { tempField: '' } } |
$inc | 数字递增(负数为递减) | { $inc: { score: 10, lives: -1 } } |
$push | 向数组末尾追加元素 | { $push: { tags: 'new_tag' } } |
$pull | 从数组中移除匹配元素 | { $pull: { tags: 'old_tag' } } |
当 data 参数中包含 $ 开头的键时,自动进入操作符模式。操作符可以组合使用:
await $data.update('players', 'player_001', {
$inc: { score: 50, combo: 1 },
$push: { achievements: 'first_win' },
$set: { lastPlayedAt: new Date().toISOString() },
})快捷方式:
$data.inc(collection, documentId, fields)等价于$data.update(collection, documentId, { $inc: fields })
幂等 / 事务方法:
| 方法 | 适合场景 | 行为 |
|---|---|---|
$data.createOnce(collection, documentId, data, ownerUid?) | 第三方事件去重、订单事件记录、一次性兑换码 | 文档不存在时创建;文档已存在时不覆盖、不报错 |
$data.incAtomic(collection, documentId, fields, options?) | 钱包余额、库存、计数器、奖励发放 | 在事务中加写锁后递增字段;字段不存在按 0 处理 |
$data.transaction(async tx => { ... }) | 支付到账、发货、扣库存等需要一组操作同时成功/失败的逻辑 | callback 成功时提交,抛错时回滚 |
incAtomic 的 fields 支持点路径:
await $data.incAtomic('wallets', caller.uid, {
balance: 100,
'stats.totalPaid': 100,
}, {
ownerUid: caller.uid,
createIfMissing: true,
})options 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
ownerUid | string | 创建新文档时使用的 owner uid |
createIfMissing | boolean | 文档不存在时是否创建;默认 false |
幂等发货示例:
const eventId = String(params.eventId || '')
const uid = String(params.uid || '')
const amount = Number(params.amount || 0)
if (!eventId || !uid || amount <= 0) return { error: 'invalid_params' }
await $data.transaction(async tx => {
const event = await tx.createOnce('payment_events', eventId, {
uid,
amount,
createdAt: Date.now(),
})
// 重复投递时 createOnce 不覆盖旧记录,直接跳过发货
if (!event.created) return
await tx.incAtomic('wallets', uid, { balance: amount }, {
ownerUid: uid,
createIfMissing: true,
})
})
return { ok: true }事务客户端支持 get、set、update、delete、inc、createOnce 和 incAtomic。需要锁定余额、库存等数值字段时,优先使用 incAtomic;它会在事务内对目标文档加写锁。不要把长时间网络请求放进事务 callback 内;需要调用第三方接口时,先在事务外完成验签和数据解析,再进入事务写库。
跨应用事务应先创建目标应用客户端,再调用 transaction:
await $data.withBasic(env.TARGET_CLIENT_ID, env.TARGET_CLIENT_SECRET).transaction(async tx => {
await tx.createOnce('events', params.eventId, { createdAt: Date.now() })
})不要在事务 callback 内再调用 tx.withBasic(...) 切换到另一个应用。
字段不存在时的行为:
| 写法 | 文档不存在 | 字段不存在 |
|---|---|---|
$data.set(collection, documentId, data, ownerUid?) | 自动创建文档 | 整个 data 替换为传入对象;不会因为缺字段报错 |
$data.update(collection, documentId, { field: value }) | 报 not_found | 自动新增顶层字段 |
$data.update(..., { $set: { field: value } }) | 报 not_found | 自动新增顶层字段 |
$data.update(..., { $inc: { field: 1 } }) | 报 not_found | 按 0 + 1 写入;$inc 的值必须是数字 |
$data.update(..., { $push: { field: value } }) | 报 not_found | 自动创建数组并追加元素;原字段不是数组时按空数组处理 |
$data.update(..., { $pull: { field: value } }) | 报 not_found | 字段不存在或不是数组时不报错,等同无操作 |
$data.update 是顶层字段更新。{ $set: { 'profile.name': 'A' } } 会写入一个名为 "profile.name" 的顶层字段,不会自动深入 profile 对象。
嵌套对象更新:
当前推荐在云函数中读出文档、修改嵌套对象,然后把变化所在的顶层字段写回:
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)这种方式会重写变化所在的顶层对象。如果多个云函数同时修改同一个顶层对象,例如都在改 settings.*,后写入可能覆盖先写入。高并发数据建议拆成更小的顶层字段或独立文档。
示例:防作弊分数上传
// 分数在服务端计算,客户端无法篡改
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 }示例:用户配置存储
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 }
}示例:原子操作(计数器 / 收藏夹)
const { playerUid, action, itemId } = params
if (action === 'like') {
// 点赞数 +1,原子操作
await $data.update('posts', params.postId, {
$inc: { likes: 1 },
})
return { success: true }
}
if (action === 'collect') {
// 向用户收藏夹添加物品
await $data.update('favorites', playerUid, {
$push: { items: itemId },
$inc: { count: 1 },
})
return { success: true }
}
if (action === 'uncollect') {
// 从收藏夹移除物品
await $data.update('favorites', playerUid, {
$pull: { items: itemId },
$inc: { count: -1 },
})
return { success: true }
}示例:排行榜查询
// 获取前 10 名玩家(按分数降序)
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
}))
// 多字段组合筛选:查找高分且高级会员玩家
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 允许云函数直接访问 Permission API 的角色和权限数据,无需发起 HTTP 请求。使用前需要在管理面板中为应用开启 Permission 服务。
所有方法返回 Promise,支持 await。
| 方法 | 说明 |
|---|---|
$permission.listRoles() | 列出所有角色 |
$permission.getRole(name) | 获取角色详情 |
$permission.listRoleMembers(roleName, { page, limit }) | 分页列出某个角色下的成员 |
$permission.getUserRoles(uid) | 获取用户的角色列表 |
$permission.getUserPermissions(uid) | 获取用户的合并权限列表 |
$permission.checkPermission(uid, permission) | 检查用户是否拥有指定权限 |
$permission.assignRole(uid, roleName) | 给用户分配角色 |
$permission.removeRole(uid, roleName) | 移除用户的角色 |
$permission.updateRolePermissions(roleName, { allow, deny }) | 更新角色的 allow/deny 权限列表 |
$permission.withBasic(clientId, clientSecret) | 创建访问其他应用 Permission 服务的客户端,返回同样的方法集合 |
$permission.asUser(uid) | 以指定用户 UID 访问当前应用 Permission 服务;也可链在 withBasic() 返回的客户端后访问目标应用 |
云函数以应用身份执行,
$data和$permission拥有完整操作权限。如需控制访问,请在代码中检查caller信息。
示例:VIP 等级自动升级
const { playerUid } = params
// 获取玩家消费记录
const record = await $data.get('purchase_records', playerUid).catch(() => null)
const totalSpent = record?.document?.data?.totalSpent || 0
// 根据消费金额判断 VIP 等级
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 }
// 移除旧 VIP 角色,分配新角色
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 }示例:权限检查中间件
// 客户端调用前先检查权限
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 — 账号只读查询
$account 提供账号相关的只读查询,适合在云函数内把 RID / 邮箱解析成 UID,或读取用户最小身份信息。它不会返回密码、验证码、2FA 密钥等敏感字段。
| 方法 | 说明 |
|---|---|
$account.getUser(uid) | 通过 UID 获取用户最小信息 |
$account.findUser({ uid, rid, email }) | 通过 UID、RID 或邮箱查询用户;按传入字段精确匹配 |
$account.getAvatar(uid) | 获取用户头像的时效链接 |
云函数内调用 $account 不受“存储/权限服务是否开启”的限制。
getUser(uid)
const user = await $account.getUser(uid)返回 null 或:
{
uid: string
rid: string
email: string
authState: number
}findUser(query)
const byRid = await $account.findUser({ rid: params.rid })
const byEmail = await $account.findUser({ email: params.email })
const byUid = await $account.findUser({ uid: params.uid })返回值与 getUser 一致。findUser 是精确匹配,不做模糊搜索。开发者可以在业务函数里允许用户输入 RID、UID 或邮箱,然后统一解析成 UID 后写入数据合集。
const input = String(params.user || '').trim()
const user = await $account.findUser({
uid: input.includes('-') ? input : undefined,
rid: input.includes('@') ? undefined : input,
email: input.includes('@') ? input : undefined,
})
if (!user || user.authState !== 1) {
return { error: 'user_not_found' }
}
await $data.set('player_profile', user.uid, { nickname: user.rid }, user.uid)
return { uid: user.uid, rid: user.rid }getAvatar(uid)
返回的 URL 带签名且有时效(按小时对齐,最长 1 小时后过期),可直接交给客户端作为 <img src="..."> 使用。
getAvatar(uid)
const result = await $account.getAvatar(uid)参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
uid | string | 是 | 目标用户的 uid |
返回值
{
uid: string // 入参回显
exists: boolean // 用户是否存在且已设置自定义头像;false 表示返回的是默认头像
url: string // 带签名的绝对 URL(http(s)://...),时效最长 1 小时
expiresAt: number // 签名过期时间(Unix 秒,整点对齐)
}示例:取当前调用者头像
if (!caller.uid) return { error: 'not_logged_in' }
const me = await $account.getAvatar(caller.uid)
return { avatar: me.url, expiresAt: me.expiresAt }示例:按 uid 取任意用户头像
const target = await $account.getAvatar(params.uid)
return {
avatar: target.url,
exists: target.exists,
expiresAt: target.expiresAt,
}示例:批量获取(并发)
const uids = params.uids || []
const results = await Promise.all(uids.map(uid => $account.getAvatar(uid)))
return { avatars: results }提示:
expiresAt是按整点对齐的签名过期时间,便于客户端/CDN 缓存。如需更长时效请在客户端临近过期时再次调用云函数刷新即可。
$storage — 文件存储桥接
$storage 允许云函数直接操作 Storage API 的文件存储,无需手动处理认证、重放防护和 HTTP 调用。使用前需要在管理面板中为应用开启 Storage 服务。
所有方法返回 Promise,支持 await。
| 方法 | 说明 |
|---|---|
$storage.getCapabilities() / getUsage() | 查询应用实际上限、分片能力、配额、预留用量和月度 URL 签发诊断;真实传输量不可用时明确返回 null |
$storage.getUploadUrl(path, size, contentType?, options?) | 获取单次上传 URL 和一次性令牌;旧调用保持兼容 |
$storage.confirmUpload(path, token) | 凭令牌确认上传,记录元数据并更新配额 |
$storage.createMultipartUpload(path, options) | 创建可续传分片会话 |
$storage.getMultipartUploadUrls(path, uploadId, partNumbers) | 批量签发分片 PUT URL |
$storage.listMultipartUploadParts(path, uploadId) | 查询 provider 已收到的分片 |
$storage.completeMultipartUpload(path, uploadId, options) | 校验分片并幂等完成 |
$storage.abortMultipartUpload(path, uploadId) | 中止未完成会话 |
$storage.listMultipartUploads(options?) | 列出分片会话 |
$storage.getDownloadUrl(path, options?) | 获取 GET 与 HEAD 下载 URL,支持响应覆盖 |
$storage.head(path) | 回读对象存储权威元数据 |
$storage.delete(path) | 删除文件 |
$storage.list(options?) | 列出文件(支持前缀、分页) |
$storage.getMeta(path) | 获取文件元信息 |
$storage.withBasic(clientId, clientSecret) | 创建访问其他应用 Storage 服务的客户端,返回同样的方法集合 |
$storage.asUser(uid) | 以指定用户 UID 访问当前应用 Storage 服务;也可链在 withBasic() 返回的客户端后访问目标应用 |
方法参数:
| 方法 | 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
getUploadUrl | path | string | ✅ | 文件路径,1~512 字符 |
size | number | ✅ | 最终 PUT Body 的字节数;默认上限 1 GiB,以 getCapabilities() 为准 | |
contentType | string | ❌ | MIME 类型,默认 application/octet-stream | |
options | object | ❌ | sha256、metadata、allowOverwrite、expiresIn | |
confirmUpload | path | string | ✅ | 文件路径(与上传时一致) |
token | string | ✅ | getUploadUrl 返回的上传令牌 | |
getDownloadUrl | path | string | ✅ | 文件路径 |
options | object | ❌ | expiresIn、downloadName、contentType、cacheControl | |
delete | path | string | ✅ | 文件路径 |
list | options.prefix | string | ❌ | 路径前缀过滤 |
options.limit | number | ❌ | 每页数量,默认 50,最大 200 | |
options.page | number | ❌ | 页码,默认 1 | |
getMeta | path | string | ✅ | 文件路径 |
返回值:
getUploadUrl 返回 { url, token, expiresIn, fullPath, requestedSize, contentType, contentLengthBound, requiredHeaders }:
| 字段 | 类型 | 说明 |
|---|---|---|
url | string | 预签名 PUT URL(默认 1 小时,以 expiresIn 为准) |
token | string | 一次性上传令牌(64 位 hex),confirmUpload 时需传入 |
expiresIn | number | URL 有效时间(秒) |
fullPath | string | 存储桶中的完整路径 |
requestedSize | number | 申请 URL 时提交的预计字节数 |
contentType | string | 申请 URL 时采用的 MIME 类型 |
contentLengthBound | boolean | 签名是否绑定精确的 HTTP Body 长度 |
requiredHeaders | object | 新式绑定启用时必须原样发送的长度和 MIME 请求头 |
getDownloadUrl 返回 { url, headUrl, expiresIn, size, contentType, etag, sha256, sha256Verification, sha256VerifyStatus }。GET URL 支持 Range;HEAD 必须使用单独的 headUrl,因为 SigV4 绑定 HTTP Method。默认 R2 对象完成后台全量回读后,sha256Verification 会由 declared 升级为 platform_verified。
| 字段 | 类型 | 说明 |
|---|---|---|
url | string | 预签名 GET URL(有效期 1 小时) |
headUrl | string | 同期签发、仅用于 HEAD 的 URL;不能与 GET URL 互换 |
sha256VerifyStatus | string | pending / processing / verified / failed |
expiresIn | number | URL 有效时间(秒) |
size | number | 文件大小(字节) |
contentType | string | MIME 类型 |
list 返回 { total, files },其中 files 数组每项包含 path、size、contentType、uploadedBy、createdAt、updatedAt。
getMeta 返回 { path, size, contentType, uploadedBy, createdAt, updatedAt }。
delete 返回 { status: 'ok' }。
⚠️ 注意:
$storage.getUploadUrl()返回预签名 URL 和一次性令牌,不会自动上传文件。新调用可传第四个参数{ bindContentLength: true }绑定精确长度;旧调用省略时保持兼容。客户端必须原样使用 URL。PUT 成功后调用$storage.confirmUpload(),平台会通过对象存储HeadObject回读真实大小和类型再更新数据库与配额;confirmUpload(path, token)不传size/contentType时直接采用实际值,云函数与旧客户端无需担心声明值与实际字节数存在细微差异。100 MiB 以上建议使用 Multipart;完整流程见 Storage API。
示例:完整上传流程
云函数接收客户端传来的文件内容,完成上传和确认:
const { fileName, fileData } = params
// fileData: base64 编码的文件内容
// 1. 先得到最终二进制 Body;byteLength 才是上传字节数
const binaryData = Uint8Array.from(atob(fileData), c => c.charCodeAt(0))
// 2. 获取上传 URL 和令牌
const uploadRes = await $storage.getUploadUrl(
`uploads/${fileName}`,
binaryData.byteLength,
params.contentType,
{ bindContentLength: true },
)
const { url, token } = uploadRes
// 3. 将文件 PUT 到预签名 URL(云函数内 fetch 可访问公网)
await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': params.contentType || 'application/octet-stream' },
body: binaryData.buffer,
})
// 4. 凭令牌确认上传
const result = await $storage.confirmUpload(`uploads/${fileName}`, token)
return { uploaded: true, path: result.path, size: result.size }示例:文件下载与分发
// 获取下载链接分发给客户端
const { url, headUrl, expiresIn, size, contentType } = await $storage.getDownloadUrl(params.filePath)
return { downloadUrl: url, headUrl, expiresIn, size, contentType }示例:列出用户相关文件
// 列出某个前缀下的所有文件
const { total, files } = await $storage.list({
prefix: `users/${caller.uid}/`,
limit: 20,
page: 1,
})
// files 包含: path, size, contentType, uploadedBy, createdAt, updatedAt
return {
total,
files: files.map(f => ({
path: f.path,
size: f.size,
contentType: f.contentType,
createdAt: f.createdAt,
})),
}示例:清理过期文件
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 }示例:用户头像存储
// 接收客户端上传的头像,存入用户专属目录
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 — 加密解密
$crypto 提供云函数内的加密专用能力。它使用 AES-256-GCM 做认证加密,适合在服务端加密需要落库或转交给客户端保存的敏感业务数据。
哈希、摘要、校验和不是加密。需要可逆保护数据时使用
$crypto.encrypt()/$crypto.decrypt();需要存储密码时应使用专门的密码哈希方案,而不是这里的对称加密。
除使用证书策略的 $crypto.jwsVerify() 外,$crypto 方法均为同步方法。策略验签可能执行 OCSP 查询,因此必须使用 await。
| 方法 | 说明 |
|---|---|
$crypto.encrypt(plaintext, key, options?) | 加密字符串或 JSON 可序列化数据 |
$crypto.decrypt(encrypted, key, options?) | 解密 $crypto.encrypt() 返回的对象或 compact 字符串 |
$crypto.generateKey(encoding?) | 生成 32 字节随机密钥,默认返回 base64 |
$crypto.randomBytes(size?, encoding?) | 生成随机字节字符串,size 范围 1~1024,默认 32 |
$crypto.hash(algorithm, data, encoding?) | 计算摘要,支持 sha1、sha256、sha512、md5 |
$crypto.hmac(algorithm, key, data, encoding?) | 计算 HMAC,支持 sha1、sha256、sha512 |
$crypto.timingSafeEqual(a, b, encoding?) | 常量时间比较两个字符串或字节数组,适合比较签名 |
$crypto.jwsSign(options) | 使用 P-256 PKCS#8 私钥生成 ES256 Compact JWS;签名固定为 JOSE `R |
$crypto.jwsVerify(options) | 使用公钥或应用级发布策略验证 ES256 JWS;策略路径同时验证 x5c 证书链、声明与吊销状态 |
hash / hmac 的输出编码支持 hex(默认)、base64、base64url。
ES256 签名示例:
const jwt = $crypto.jwsSign({
algorithm: 'ES256',
key: $secrets.ref('PARTNER_SIGNING_PRIVATE_KEY'),
header: { kid: env.PARTNER_SIGNING_KEY_ID, typ: 'JWT' },
payload: { iss: env.PARTNER_ISSUER_ID, aud: 'partner-api' },
})需要验证 x5c 证书链时,云函数只能引用当前应用中已发布、固定版本的 JWS/X.509 策略。证书集合、证书位置约束、验证时间、声明规则和吊销模式由策略定义,运行时不会按 Provider 名称选择隐藏信任库。返回内容只含结构化 Header/Payload、策略摘要和证书摘要, 不含证书正文:
const verified = await $crypto.jwsVerify({
signedPayload: params.signedPayload,
algorithm: 'ES256',
policy: $certificates.policy('partner_webhook_jws_v1', 1),
})策略名和版本必须是部署时固定值,不能从 params、请求 Body、Header 或 Query 中读取。 旧 trustStore / expected / validateAt 参数已移除,不存在自动回退;原有云函数必须改用发布策略。 若只需要验证一个固定公钥,可继续传 key: $secrets.ref('PUBLIC_KEY'),但该路径不会验证 x5c 证书链、策略声明或吊销状态。
策略验签失败会抛出结构化错误:
{
code: string
retryable: boolean
stage: string
category: string
}retryable=true 表示 OCSP 等外部依赖暂时不可用,Raw HTTP/Webhook 应返回 503 并允许 上游重试;签名、证书链、声明或配置错误属于永久失败,应返回 400。不要解析错误文案来 决定重试。OCSP 明确返回 revoked 或 unknown 时也属于永久拒绝。
options 参数:
| 字段 | 类型 | 说明 |
|---|---|---|
encoding | string | 密文、IV、认证标签输出编码:'base64'(默认)或 'hex' |
keyEncoding | string | 密钥输入编码:'utf8'(默认)、'base64' 或 'hex' |
aad | string | 附加认证数据,解密时必须传入相同值 |
parseJson | boolean | 仅 decrypt 使用;为 true 时额外返回 data: JSON.parse(plaintext) |
返回值:
encrypt 返回:
{
algorithm: 'aes-256-gcm'
encoding: 'base64' | 'hex'
iv: string
tag: string
ciphertext: string
compact: string
}compact 是 v1:<encoding>:<iv>:<tag>:<ciphertext> 形式,便于作为一个字符串保存。decrypt 可以直接接收该字符串,也可以接收完整对象。
示例:生成密钥
const key = $crypto.generateKey('base64')
return {
key,
keyEncoding: 'base64',
note: '将 key 保存到 env.CRYPTO_KEY,将 keyEncoding 保存到 env.CRYPTO_KEY_ENCODING',
}新密钥应写入应用级 Secret Store 并通过
$secrets.ref()使用。env.KEY仅为既有函数保留兼容,不能把 Secret、JWT、完整 JWS 或证书写入日志。
示例:Webhook 签名校验
显式设置 requestMode=raw 的 Raw HTTP 云函数会提供解析前捕获的 request.rawBody、 精确二进制 request.rawBodyBase64 和小写 key 的 request.headers,可直接用于第三方回调验签:
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: '' }使用 HMAC 的第三方平台可以这样写:
const signature = String(request.headers?.['x-signature'] || '')
const expected = $crypto.hmac('sha256', env.WEBHOOK_SECRET, request.rawBody, 'hex')
if (!$crypto.timingSafeEqual(signature, expected)) {
return { statusCode: 401, body: { error: 'invalid_signature' } }
}示例:加密并保存到 Data
const key = env.CRYPTO_KEY
const keyEncoding = env.CRYPTO_KEY_ENCODING || 'utf8'
const encrypted = $crypto.encrypt(params.secretProfile, key, {
keyEncoding,
aad: caller.uid,
})
await $data.set('secret_profiles', caller.uid, {
encrypted: encrypted.compact,
updatedAt: new Date().toISOString(),
}, caller.uid)
return { saved: true }示例:读取并解密
const key = env.CRYPTO_KEY
const keyEncoding = env.CRYPTO_KEY_ENCODING || 'utf8'
const doc = await $data.get('secret_profiles', caller.uid)
const decrypted = $crypto.decrypt(doc.document.data.encrypted, key, {
keyEncoding,
aad: caller.uid,
parseJson: true,
})
return { profile: decrypted.data }使用
aad时,解密必须传入完全相同的aad。密文、IV、认证标签或aad任意一项被篡改,解密都会失败。
$certificates — JWS/X.509 发布策略
$certificates.policy(name, version) 返回不可伪造的策略引用,只能解析当前应用同名、同版本 且状态为 published 的策略。$certificates.hasPolicy(name, version) 可用于部署自检,但不能 替代固定版本引用。已发布的证书集合和策略不可原地修改;轮换证书或规则时应创建、发布新 版本,更新云函数中的版本字面量,验证后再弃用旧版本。
if (!$certificates.hasPolicy('partner_webhook_jws_v1', 1)) {
throw new Error('Required certificate policy is not published')
}
const verified = await $crypto.jwsVerify({
signedPayload: params.signedPayload,
algorithm: 'ES256',
policy: $certificates.policy('partner_webhook_jws_v1', 1),
})运行时只加载源码中由 $certificates.policy('name', version) 直接固定引用的策略;不要把 名称或版本先放入变量、env 或请求字段再传入。这样外部输入无法选择或降低信任策略, 同时也不会把应用的其他证书集合无条件复制进隔离 Worker。
应用所有者通过 Cloud Function 管理 API 管理资源:
| 资源 | 草稿与查询 | 发布 / 弃用 |
|---|---|---|
| 证书集合 | GET/POST /cf/manage/{clientId}/certificate-bundles、GET/PUT/DELETE .../certificate-bundles/{bundleId} | POST .../certificate-bundles/{bundleId}/publish、POST .../deprecate |
| JWS/X.509 策略 | GET/POST /cf/manage/{clientId}/jws-x509-policies、GET/PUT/DELETE .../jws-x509-policies/{policyId} | POST .../jws-x509-policies/{policyId}/publish、POST .../deprecate |
应用所有者和平台管理员可以写入;具备 Cloud Function 查看权限的协作者只能读取。每次 创建、修改、发布、弃用和删除都会进入应用操作日志;证书集合日志只记录 SHA-256 指纹, 不会保存完整 PEM。
创建证书集合时提交稳定名称和 1~32 个 PEM X.509 CA 证书;平台自动分配版本。发布证书 集合后,以返回对象中的 bundle.id 作为 certificateBundleId 创建策略草稿:
{
"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 支持严格类型匹配的 equals / oneOf、存在或缺失检查,以及 requiredWhen 条件规则。把应用身份、环境和允许的业务标识写入发布策略,不要由每次 请求传入这些信任边界条件。
$secrets — 只写 Secret 引用
$secrets.ref(name) 返回不透明引用,供 $crypto.jwsSign、$crypto.jwsVerify 等受管能力 解析;它不会返回 Secret 字符串。has(name) 用于检查是否配置,names() 只返回名称。 应用级 Secret 同时合并进 env.NAME,确保旧函数迁移后继续运行。
管理 API 的 list/get/put 响应只含名称、isSet 和时间戳。空字符串会登记一个 isSet=false 的待配置 Key;此时 has() 返回 false,ref() 会报未配置。函数级脱敏 占位符仍保留现值,删除必须显式调用 DELETE /cf/manage/{clientId}/secrets/{NAME}。
控制台日志、错误和持久化执行记录会屏蔽 Secret;云函数返回值不做字符替换, 因为预签名 URL、签名、密文和不透明 token 必须逐字节保持。不要在返回值中显式暴露 Secret。
$appleIap — Apple IAP 服务端能力
$appleIap 在通用 ES256/X.509 能力上封装 App Store Server API:
| 方法 | 说明 |
|---|---|
createApiToken(options) | 创建 Apple 要求的 ES256 API JWT |
getTransactionInfo(options) | 调用 Get Transaction Info,并使用调用方指定的发布策略验证交易 JWS |
verifyNotification(options) | 兼容用薄封装;新函数应使用 $crypto.jwsVerify() 显式验证每一层 JWS |
getTransactionInfo 的 environment: 'auto' 会先查询 Production;仅在 Apple 返回 4040010(交易不存在)时查询 Sandbox。GET 可配置每次请求超时和最多 2 次有限重试, 返回值始终带明确环境。
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,
})模板优先使用 Secret 名称 APPLE_IAP_PRIVATE_KEY_P8,并兼容既有名称 APPLE_IAP_PRIVATE_KEY;privateKeySecret 本身可指定任意符合 Secret 命名规则的名称。
Notifications V2 不依赖 Provider 专用验签分支。外层通知、内嵌交易和内嵌续订必须分别 await $crypto.jwsVerify(),并直接引用部署时固定的发布策略:
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
: nullProduction 通知必须配置正整数 APPLE_IAP_APP_APPLE_ID,并在验签后用严格数字类型匹配 data.appAppleId;Sandbox 可以不配置该值。Bundle ID、允许商品、环境和 App Apple ID 还应 作为静态 expectedClaims 固化到对应发布策略中。策略名和版本不能来自 env 或请求参数。 任一层出现 retryable=true 时 Webhook 返回 503,永久验签或声明失败返回 400;具体错误 响应写法见模板中心的“Apple IAP 服务端验单”。
模板中心的“Apple IAP 服务端验单”同时示范了 payment_events + $data.transaction/createOnce 的通知与验单幂等。业务发货/退款应放在同一事务内,审计只 保存 Provider、事件 ID、交易 ID、UID、商品、环境和状态,不保存完整 JWS。
$invoke — 函数互调
$invoke 允许云函数同步调用同应用内的其他云函数,并获取返回值。通过 withBasic 可以调用其他应用的云函数;通过 asUser(uid) 可以让被调用函数以指定用户身份看到 caller.mode === 'user' 和对应 caller.uid。
如果某个函数只应作为内部 helper 使用,可以在函数设置中把 invokeAccess 设置为 cloud_function。此时该函数不会接受用户、匿名访问、Basic/Bearer HTTP 请求或定时任务, 只能由其他云函数通过 $invoke 调用。
const result = await $invoke(functionNameOrPath, params?, folder?)| 参数 | 类型 | 说明 |
|---|---|---|
functionNameOrPath | string | 目标函数名称,或 folder/functionName 路径 |
params | object | 传递参数(可选) |
folder | string | 目标函数的文件夹路径(可选);如果第一个参数已经是路径,可省略 |
返回值: 目标函数的返回值。
跨应用调用:
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')
// 等价写法:
// const result = await invokePartner('shared/get_exchange_rate', { from: 'gold', to: 'diamond' })限制:
- 每次函数执行最多调用 10 次
- 最大嵌套深度:5 层(函数 A → B → C → D → E)
- 默认目标函数必须处于启用状态且在同一应用内
- 跨应用调用时使用
$invoke.withBasic(clientId, clientSecret),目标应用必须审核通过并开通 Cloud Function 服务
示例:复用共享逻辑
// 函数: create_order
const { items, buyerUid } = params
// 调用共享验证函数
const validation = await $invoke('validate_items', { items })
if (!validation.valid) return { error: validation.reason }
// 调用定价函数(位于 utils 文件夹)
const pricing = await $invoke('calculate_price', { items }, 'utils')
// 也可以写成:
// const pricing = await $invoke('utils/calculate_price', { items })
return { orderId: 'xxx', total: pricing.total }示例:跨应用调用云函数
调用其他应用的云函数时,可以使用 $invoke.withBasic() 传入目标应用的 Client ID 和 Client Secret。平台会按 Basic 认证校验目标应用凭据。
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')
// 也可以写成 invokePartner('shared/get_exchange_rate', { from: 'gold', to: 'diamond' })
const rate = result?.rate || 1
return { exchangeRate: rate }提示:仍可使用
fetch()调用外部 Cloud Function API,但内置$invoke.withBasic()不需要手动拼接Authorization、X-Timestamp和X-Nonce。
fetch — HTTP 请求
云函数可以使用 fetch() 向公网发起 HTTP/HTTPS 请求。
限制:
- 仅允许
http://和https://协议 - 禁止访问内网地址(
localhost、127.0.0.1、10.*、192.168.*、172.16-31.*、IPv6 回环/链路本地/ULA 地址等) timeoutMs可配置 1~15 秒,默认 10 秒- 仅幂等 GET 支持
retries: 0~2有限重试;其他方法不会自动重试 - 响应体上限 1 MB
返回值:
| 字段 | 类型 | 说明 |
|---|---|---|
ok | boolean | 状态码是否为 2xx |
status | number | HTTP 状态码 |
text | string | 响应文本 |
json | any | 自动解析的 JSON(解析失败为 undefined) |
headers | object | 响应头 |
// 调用外部 API
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