{"record":{"id":"3a0350a43b867a93","repo":"can1357/oh-my-pi","slug":"z-ai-key-provisioning-returned-no-secretkey","errorCode":null,"errorMessage":"Z.ai key provisioning returned no secretKey","messagePattern":"Z\\.ai key provisioning returned no secretKey","errorType":"exception","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/zai.ts","lineNumber":207,"sourceCode":"\n\tconst apiKey = trimmedString(keyRecord?.apiKey);\n\tif (!apiKey) {\n\t\tthrow new AIError.OAuthError(\"Z.ai key provisioning returned no apiKey\", {\n\t\t\tkind: \"token-exchange\",\n\t\t\tprovider: \"zai\",\n\t\t});\n\t}\n\n\t// Always fetch the secret via the copy endpoint: list entries mask it\n\t// (`*****abcd`) and the create response's inline secret is not reliable\n\t// across account states, whereas copy always returns the full secret.\n\tconst copied = unwrapEnvelope(\n\t\tawait getJson(`${keysUrl}/copy/${encodeURIComponent(apiKey)}`, auth, fetchImpl),\n\t\t\"api key copy\",\n\t) as { secretKey?: unknown } | undefined;\n\tconst secretKey = trimmedString(copied?.secretKey);\n\tif (!secretKey) {\n\t\tthrow new AIError.OAuthError(\"Z.ai key provisioning returned no secretKey\", {\n\t\t\tkind: \"token-exchange\",\n\t\t\tprovider: \"zai\",\n\t\t});\n\t}\n\n\treturn `${apiKey}.${secretKey}`;\n}\n\nexport class ZaiOAuthFlow extends OAuthCallbackFlow {\n\t#fetch: FetchImpl;\n\n\tconstructor(ctrl: OAuthController) {\n\t\tsuper(ctrl, {\n\t\t\tpreferredPort: CALLBACK_PORT,\n\t\t\tcallbackPath: CALLBACK_PATH,\n\t\t\tallowPortFallback: false,\n\t\t});\n\t\tthis.#fetch = ctrl.fetch ?? fetch;","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/zai.ts#L189-L225","documentation":"Listed key entries mask the secret, so mintZaiApiKey always fetches the plaintext via the copy endpoint (/copy/{apiKey}) and requires a non-empty secretKey in the response. If absent, OAuthError is thrown — the key identifier exists but its secret could not be retrieved, leaving an unusable half-credential.","triggerScenarios":"Copy endpoint returns envelope without secretKey: insufficient permission to copy, key created in a different region than the copy request, Z.ai changed the field name, or the copy endpoint rejects the URL-encoded key.","commonSituations":"Region mismatch between key creation and copy call; token lacking copy scope; Z.ai API schema drift; key deleted between create and copy.","solutions":["Ensure the same regional BIZ_BASE and auth are used for create and copy; re-run the full mint flow.","Check the account/token has permission to copy (reveal) key secrets — some roles can create but not reveal.","Log the copy response to detect renamed fields and update the library if the schema changed.","On failure, delete the orphaned apiKey via the API to avoid accumulating unusable keys."],"exampleFix":"// before\nconst copied = await getJson(`${keysUrl}/copy/${encodeURIComponent(apiKey)}`, auth, fetch);\n// after\nlet copied = await getJson(`${keysUrl}/copy/${encodeURIComponent(apiKey)}`, auth, fetch).catch(async e => {\n  await deleteZaiKey(keysUrl, apiKey, auth, fetch); // clean up unusable key\n  throw e;\n});","handlingStrategy":"try-catch","validationCode":"if (!apiKey) throw new Error(\"Cannot fetch secret: apiKey missing from create response\");","typeGuard":"function hasSecretKey(r: unknown): r is { secretKey: string } { return !!r && typeof r === \"object\" && typeof (r as Record<string, unknown>).secretKey === \"string\" && (r as { secretKey: string }).secretKey.length > 0; }","tryCatchPattern":"try { return await mintZaiApiKey(token, fetch); }\ncatch (e) {\n  if (e instanceof AIError.OAuthError && e.message.includes(\"no secretKey\")) {\n    await deleteZaiKeySafely(token, apiKey).catch(() => {}); // avoid orphaned key\n    return retryMintOnce();\n  }\n  throw e;\n}","preventionTips":["Use one consistent regional base URL for create and copy calls","Verify the token can reveal (copy) key secrets, not just create","Clean up unusable keys after failed mints to stay under quota","Alert on this error since the key identifier exists without a retrievable secret"],"tags":["zai","provisioning","secret-key","copy-endpoint","missing-field"],"backgroundTag":"missing-credential-in-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}