长期、全账户计费密钥被以原始文本保存在应用状态中,并默认跨升级保留
原文依据:4 处实现把用户提交的 `sk-...` 直接作为 `Text` 放入 canister 映射或单一变量。说明将“保留”列为几乎所有应用的默认升级策略,同时指出该密钥长期有效、无权限范围且可消耗整个账户余额。
密钥在应用中的暴露面和保存时间被扩大。如果未来的端点、升级、状态处理或管理访问泄露这些值,受影响的不只是一次会话,而是相应 OpenAI 账户及其预算。
源码确实要求把原始密钥作为 `Text` 存入 canister 状态,并称跨升级保留是默认选择。这会延长高权限、长期计费凭据的保存期;若 canister 状态、升级流程或依赖出现问题,影响可能涉及整个 OpenAI 账户。不过技能也明确禁止 getter、日志和前端回读,因此证据不表明普通用户能够直接读取密钥。用户可要求说明静态存储保护、轮换和删除策略。
- Long-lived, no expiry. Spends the entire OpenAI account balance on every call.- No scoped permissions — there is no "tweet.read"-style narrowing. Every key has full account access.- OpenAI rate-limits per-key per-minute; treat the key like a billing credential, not a session token.- **Never returned by any `query` or `shared` function.** Never logged. Never sent to the frontend. Never put in a stable variable that another endpoint with a weaker gate could read.查看另外 3 个位置
// Per-user OpenAI keys. Never iterated except by the calling principal. let openAIKeys : Map.Map<Principal, Text>; include MixinOpenAIChat(openAIKeys);}; public shared ({ caller }) func setMyOpenAIApiKey(key : Text) : async () { if (caller.isAnonymous()) { Runtime.trap("Sign in to use this feature"); }; openAIKeys.add(caller, key); };- **Anonymous callers must not store keys.** `caller.isAnonymous()` short-circuits before any `openAIKeys.add` — otherwise everyone reading the canister via `2vxsx-fae` shares one key slot.- **`stable var` / migration.** The `Map<Principal, Text>` lives in stable memory like any other actor field; on upgrade, decide whether to preserve, rotate, or drop the keys. The default (preserve) is correct for almost all apps. If you ever rotate, drop the whole map — never partially.