bkper 5.8.2 → 5.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
1
  import { createRequire as __bkperCreateRequire } from "node:module"; const require = __bkperCreateRequire(import.meta.url);
2
- var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is not supported')});var procEnvCache=null;function getBunSandboxEnvValue(name){if(!(typeof process>"u"||!process.versions?.bun||Object.keys(process.env).length>0)){if(procEnvCache===null){procEnvCache=new Map;try{let{readFileSync}=__require("node:fs"),data=readFileSync("/proc/self/environ","utf-8");for(let entry of data.split("\0")){let idx=entry.indexOf("=");idx>0&&procEnvCache.set(entry.slice(0,idx),entry.slice(idx+1))}}catch{}}return procEnvCache.get(name)}}function getProviderEnvValue(name,env){return env?.[name]||(typeof process<"u"?process.env[name]:void 0)||getBunSandboxEnvValue(name)||void 0}function sleep(ms,signal){return new Promise((resolve,reject)=>{signal.throwIfAborted();let onAbort=()=>{clearTimeout(timeout),reject(signal.reason)},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}var CLIENT_ID="17e5f671-d194-4dfb-9706-5516cb48c098",DEFAULT_OAUTH_HOST="https://auth.kimi.com",DEVICE_CODE_TIMEOUT_SECONDS=900,DEFAULT_POLL_INTERVAL_SECONDS=5,REQUEST_TIMEOUT_MS=30*1e3,REFRESH_MAX_RETRIES=3;function getOauthHost(){return(getProviderEnvValue("KIMI_CODE_OAUTH_HOST")||getProviderEnvValue("KIMI_OAUTH_HOST")||DEFAULT_OAUTH_HOST).replace(/\/+$/,"")}function requestSignal(signal){return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS),signal])}function formUrlEncode(fields){return new URLSearchParams(fields).toString()}async function readJson(response){try{let json=await response.json();return json&&typeof json=="object"?json:null}catch{return null}}function trustedHttpUrl(value){if(typeof value!="string"||!value)return null;try{let url=new URL(value);return url.protocol!=="https:"&&url.protocol!=="http:"?null:url.href}catch{return null}}async function startDeviceAuthorization(oauthHost,signal){let response=await fetch(`${oauthHost}/api/oauth/device_authorization`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID}),signal:requestSignal(signal)});if(!response.ok){let text=await response.text().catch(()=>"");throw new Error(`Kimi Code device authorization failed with status ${response.status}${text?`: ${text}`:""}`)}let json=await readJson(response),deviceCode=json?.device_code,userCode=json?.user_code,verificationUri=json?.verification_uri,verificationUriComplete=json?.verification_uri_complete;if(typeof deviceCode!="string"||typeof userCode!="string"||typeof verificationUri!="string"||typeof verificationUriComplete!="string"||!trustedHttpUrl(verificationUriComplete)||!trustedHttpUrl(verificationUri))throw new Error(`Invalid Kimi Code device authorization response: ${JSON.stringify(json)}`);let interval=json?.interval,expiresIn=json?.expires_in;return{deviceCode,userCode,verificationUri,verificationUriComplete,intervalSeconds:typeof interval=="number"&&Number.isFinite(interval)&&interval>0?interval:DEFAULT_POLL_INTERVAL_SECONDS,expiresInSeconds:typeof expiresIn=="number"&&Number.isFinite(expiresIn)&&expiresIn>0?expiresIn:DEVICE_CODE_TIMEOUT_SECONDS}}function parseTokenResponse(json,operation){let accessToken=json?.access_token,refreshToken2=json?.refresh_token,expiresIn=json?.expires_in;if(typeof accessToken!="string"||!accessToken||typeof refreshToken2!="string"||!refreshToken2||typeof expiresIn!="number"||!Number.isFinite(expiresIn)||expiresIn<=0)throw new Error(`Kimi Code token ${operation} response missing fields: ${JSON.stringify(json)}`);return{access:accessToken,refresh:refreshToken2,expires:Date.now()+expiresIn*1e3}}async function pollForToken(oauthHost,device,signal){return pollOAuthDeviceCodeFlow({intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds,waitBeforeFirstPoll:!0,signal,poll:async()=>{let response=await fetch(`${oauthHost}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID,device_code:device.deviceCode,grant_type:"urn:ietf:params:oauth:grant-type:device_code"}),signal:requestSignal(signal)});if(response.status>=500){let text=await response.text().catch(()=>"");return{status:"failed",message:`Kimi Code device token request failed with status ${response.status}${text?`: ${text}`:""}`}}let json=await readJson(response);if(response.ok&&typeof json?.access_token=="string")try{return{status:"complete",value:parseTokenResponse(json,"poll")}}catch(error2){return{status:"failed",message:error2 instanceof Error?error2.message:String(error2)}}let error=json?.error,description=typeof json?.error_description=="string"?`: ${json.error_description}`:"";if(error==="authorization_pending")return{status:"pending"};if(error==="slow_down"){let interval=json?.interval;return{status:"slow_down",intervalSeconds:typeof interval=="number"&&interval>0?interval:void 0}}return error==="expired_token"?{status:"failed",message:"Kimi Code device authorization expired. Please restart login."}:error==="access_denied"?{status:"failed",message:"Kimi Code login was denied."}:{status:"failed",message:`Kimi Code device token request failed (status ${response.status})${typeof error=="string"?`: ${error}${description}`:""}`}}})}function isRetryableRefreshFailure(response){return response.status===429||response.status>=500}async function refreshToken(oauthHost,refreshTokenValue,signal){let lastError;for(let attempt=0;attempt<=REFRESH_MAX_RETRIES;attempt++){if(attempt>0&&await sleep(1e3*2**(attempt-1),signal),signal.aborted)throw new Error("Kimi Code token refresh aborted");let response;try{response=await fetch(`${oauthHost}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID,grant_type:"refresh_token",refresh_token:refreshTokenValue}),signal:requestSignal(signal)})}catch(error){lastError=error instanceof Error?error:new Error(String(error));continue}let json=await readJson(response);if(response.ok)return parseTokenResponse(json,"refresh");if(response.status===401||response.status===403||json?.error==="invalid_grant"){let description=typeof json?.error_description=="string"?`: ${json.error_description}`:"";throw new Error(`Kimi Code token refresh unauthorized (status ${response.status})${description}`)}if(isRetryableRefreshFailure(response)&&attempt<REFRESH_MAX_RETRIES){lastError=new Error(`Kimi Code token refresh failed with status ${response.status}`);continue}let text=JSON.stringify(json);throw new Error(`Kimi Code token refresh failed with status ${response.status}${text?`: ${text}`:""}`)}throw lastError??new Error("Kimi Code token refresh failed")}async function loginKimiCoding(interaction){let oauthHost=getOauthHost(),device=await startDeviceAuthorization(oauthHost,interaction.signal);interaction.notify({type:"device_code",userCode:device.userCode,verificationUri:device.verificationUriComplete,intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds});let token=await pollForToken(oauthHost,device,interaction.signal);return{type:"oauth",access:token.access,refresh:token.refresh,expires:token.expires}}var kimiCodingOAuth={name:"Kimi Code (subscription)",isSubscription:!0,loginLabel:"Sign in with Kimi Code",login:loginKimiCoding,refresh:async(credential,signal)=>{let token=await refreshToken(getOauthHost(),credential.refresh,signal);return{type:"oauth",access:token.access,refresh:token.refresh,expires:token.expires}},async toAuth(credential){return{headers:{Authorization:`Bearer ${credential.access}`}}}};export{kimiCodingOAuth};
2
+ var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is not supported')});var procEnvCache=null;function getBunSandboxEnvValue(name){if(!(typeof process>"u"||!process.versions?.bun||Object.keys(process.env).length>0)){if(procEnvCache===null){procEnvCache=new Map;try{let{readFileSync}=__require("node:fs"),data=readFileSync("/proc/self/environ","utf-8");for(let entry of data.split("\0")){let idx=entry.indexOf("=");idx>0&&procEnvCache.set(entry.slice(0,idx),entry.slice(idx+1))}}catch{}}return procEnvCache.get(name)}}function getProviderEnvValue(name,env){return env?.[name]||(typeof process<"u"?process.env[name]:void 0)||getBunSandboxEnvValue(name)||void 0}function sleep(ms,signal){return new Promise((resolve,reject)=>{signal.throwIfAborted();let onAbort=()=>{clearTimeout(timeout),reject(signal.reason)},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}function trustedHttpUrl(value){if(typeof value!="string"||!URL.canParse(value))return null;let url=new URL(value);return url.protocol==="https:"||url.protocol==="http:"?url.href:null}var CLIENT_ID="17e5f671-d194-4dfb-9706-5516cb48c098",DEFAULT_OAUTH_HOST="https://auth.kimi.com",DEVICE_CODE_TIMEOUT_SECONDS=900,DEFAULT_POLL_INTERVAL_SECONDS=5,REQUEST_TIMEOUT_MS=30*1e3,REFRESH_MAX_RETRIES=3;function getOauthHost(){return(getProviderEnvValue("KIMI_CODE_OAUTH_HOST")||getProviderEnvValue("KIMI_OAUTH_HOST")||DEFAULT_OAUTH_HOST).replace(/\/+$/,"")}function requestSignal(signal){return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS),signal])}function formUrlEncode(fields){return new URLSearchParams(fields).toString()}async function readJson(response){try{let json=await response.json();return json&&typeof json=="object"?json:null}catch{return null}}async function startDeviceAuthorization(oauthHost,signal){let response=await fetch(`${oauthHost}/api/oauth/device_authorization`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID}),signal:requestSignal(signal)});if(!response.ok){let text=await response.text().catch(()=>"");throw new Error(`Kimi Code device authorization failed with status ${response.status}${text?`: ${text}`:""}`)}let json=await readJson(response),deviceCode=json?.device_code,userCode=json?.user_code,verificationUri=trustedHttpUrl(json?.verification_uri),verificationUriComplete=trustedHttpUrl(json?.verification_uri_complete);if(typeof deviceCode!="string"||typeof userCode!="string"||!verificationUri||!verificationUriComplete)throw new Error(`Invalid Kimi Code device authorization response: ${JSON.stringify(json)}`);let interval=json?.interval,expiresIn=json?.expires_in;return{deviceCode,userCode,verificationUri,verificationUriComplete,intervalSeconds:typeof interval=="number"&&Number.isFinite(interval)&&interval>0?interval:DEFAULT_POLL_INTERVAL_SECONDS,expiresInSeconds:typeof expiresIn=="number"&&Number.isFinite(expiresIn)&&expiresIn>0?expiresIn:DEVICE_CODE_TIMEOUT_SECONDS}}function parseTokenResponse(json,operation){let accessToken=json?.access_token,refreshToken2=json?.refresh_token,expiresIn=json?.expires_in;if(typeof accessToken!="string"||!accessToken||typeof refreshToken2!="string"||!refreshToken2||typeof expiresIn!="number"||!Number.isFinite(expiresIn)||expiresIn<=0)throw new Error(`Kimi Code token ${operation} response missing fields: ${JSON.stringify(json)}`);return{access:accessToken,refresh:refreshToken2,expires:Date.now()+expiresIn*1e3}}async function pollForToken(oauthHost,device,signal){return pollOAuthDeviceCodeFlow({intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds,waitBeforeFirstPoll:!0,signal,poll:async()=>{let response=await fetch(`${oauthHost}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID,device_code:device.deviceCode,grant_type:"urn:ietf:params:oauth:grant-type:device_code"}),signal:requestSignal(signal)});if(response.status>=500){let text=await response.text().catch(()=>"");return{status:"failed",message:`Kimi Code device token request failed with status ${response.status}${text?`: ${text}`:""}`}}let json=await readJson(response);if(response.ok&&typeof json?.access_token=="string")try{return{status:"complete",value:parseTokenResponse(json,"poll")}}catch(error2){return{status:"failed",message:error2 instanceof Error?error2.message:String(error2)}}let error=json?.error,description=typeof json?.error_description=="string"?`: ${json.error_description}`:"";if(error==="authorization_pending")return{status:"pending"};if(error==="slow_down"){let interval=json?.interval;return{status:"slow_down",intervalSeconds:typeof interval=="number"&&interval>0?interval:void 0}}return error==="expired_token"?{status:"failed",message:"Kimi Code device authorization expired. Please restart login."}:error==="access_denied"?{status:"failed",message:"Kimi Code login was denied."}:{status:"failed",message:`Kimi Code device token request failed (status ${response.status})${typeof error=="string"?`: ${error}${description}`:""}`}}})}function isRetryableRefreshFailure(response){return response.status===429||response.status>=500}async function refreshToken(oauthHost,refreshTokenValue,signal){let lastError;for(let attempt=0;attempt<=REFRESH_MAX_RETRIES;attempt++){if(attempt>0&&await sleep(1e3*2**(attempt-1),signal),signal.aborted)throw new Error("Kimi Code token refresh aborted");let response;try{response=await fetch(`${oauthHost}/api/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:formUrlEncode({client_id:CLIENT_ID,grant_type:"refresh_token",refresh_token:refreshTokenValue}),signal:requestSignal(signal)})}catch(error){lastError=error instanceof Error?error:new Error(String(error));continue}let json=await readJson(response);if(response.ok)return parseTokenResponse(json,"refresh");if(response.status===401||response.status===403||json?.error==="invalid_grant"){let description=typeof json?.error_description=="string"?`: ${json.error_description}`:"";throw new Error(`Kimi Code token refresh unauthorized (status ${response.status})${description}`)}if(isRetryableRefreshFailure(response)&&attempt<REFRESH_MAX_RETRIES){lastError=new Error(`Kimi Code token refresh failed with status ${response.status}`);continue}let text=JSON.stringify(json);throw new Error(`Kimi Code token refresh failed with status ${response.status}${text?`: ${text}`:""}`)}throw lastError??new Error("Kimi Code token refresh failed")}async function loginKimiCoding(interaction){let oauthHost=getOauthHost(),device=await startDeviceAuthorization(oauthHost,interaction.signal);interaction.notify({type:"device_code",userCode:device.userCode,verificationUri:device.verificationUriComplete,intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds});let token=await pollForToken(oauthHost,device,interaction.signal);return{type:"oauth",access:token.access,refresh:token.refresh,expires:token.expires}}var kimiCodingOAuth={name:"Kimi Code (subscription)",isSubscription:!0,loginLabel:"Sign in with Kimi Code",login:loginKimiCoding,refresh:async(credential,signal)=>{let token=await refreshToken(getOauthHost(),credential.refresh,signal);return{type:"oauth",access:token.access,refresh:token.refresh,expires:token.expires}},async toAuth(credential){return{headers:{Authorization:`Bearer ${credential.access}`}}}};export{kimiCodingOAuth};
@@ -1,2 +1,2 @@
1
1
  import { createRequire as __bkperCreateRequire } from "node:module"; const require = __bkperCreateRequire(import.meta.url);
2
- var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}var CLIENT_ID="1031625952748946",AUTH_HOST="https://auth.meta.com",DEVICE_AUTHORIZATION_URL=`${AUTH_HOST}/oidc/device/authorization/`,DEVICE_TOKEN_URL=`${AUTH_HOST}/oidc/device/token/`,API_KEY_MINT_URL="https://api.meta.ai/muse-code/key",API_KEY_LIFETIME_MS=1440*60*1e3,REQUEST_TIMEOUT_MS=30*1e3;function requestSignal(signal){return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS),signal])}async function readJson(response){try{let json=await response.json();return json&&typeof json=="object"?json:null}catch{return null}}function errorDetail(json){for(let key of["error_description","detail","message","error"]){let value=json?.[key];if(typeof value=="string"&&value.trim())return`: ${value.trim()}`}return""}function trustedHttpUrl(value){if(typeof value!="string"||!value)return null;try{let url=new URL(value);return url.protocol!=="https:"&&url.protocol!=="http:"?null:url.href}catch{return null}}function positiveNumber(value){return typeof value=="number"&&Number.isFinite(value)&&value>0?value:void 0}async function startDeviceAuthorization(signal){let response=await fetch(DEVICE_AUTHORIZATION_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({client_id:CLIENT_ID}).toString(),signal:requestSignal(signal)}),json=await readJson(response);if(!response.ok)throw new Error(`Meta device authorization failed with status ${response.status}${errorDetail(json)}`);let deviceCode=json?.device_code,userCode=json?.user_code,verificationUri=trustedHttpUrl(json?.verification_uri_complete)??trustedHttpUrl(json?.verification_uri);if(typeof deviceCode!="string"||!deviceCode||typeof userCode!="string"||!userCode||!verificationUri)throw new Error(`Invalid Meta device authorization response: ${JSON.stringify(json)}`);return{deviceCode,userCode,verificationUri,intervalSeconds:positiveNumber(json?.interval),expiresInSeconds:positiveNumber(json?.expires_in)}}async function pollForIdentityToken(device,signal){return pollOAuthDeviceCodeFlow({intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds,waitBeforeFirstPoll:!0,signal,poll:async()=>{let response=await fetch(DEVICE_TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:device_code",device_code:device.deviceCode,client_id:CLIENT_ID}).toString(),signal:requestSignal(signal)}),json=await readJson(response);if(response.ok&&typeof json?.access_token=="string"&&json.access_token)return{status:"complete",value:json.access_token};switch(json?.error){case"authorization_pending":return{status:"pending"};case"slow_down":return{status:"slow_down",intervalSeconds:positiveNumber(json?.interval)};case"access_denied":return{status:"failed",message:"Meta login was denied."};case"expired_token":return{status:"failed",message:"Meta device authorization expired. Please restart login."};default:return{status:"failed",message:`Meta device token request failed with status ${response.status}${errorDetail(json)}`}}}})}async function mintApiKey(identityToken,signal){let response=await fetch(API_KEY_MINT_URL,{method:"POST",headers:{Accept:"application/json",Authorization:`Bearer ${identityToken}`,"Content-Type":"application/json","x-api-version":"1.0.0"},body:"{}",signal:requestSignal(signal)}),json=await readJson(response);if(response.status===401||response.status===403)throw new Error(`Meta session expired (status ${response.status}). Run \`/login meta\` to sign in again.${errorDetail(json)}`);if(!response.ok)throw new Error(`Meta API key mint failed with status ${response.status}${errorDetail(json)}`);let apiKey=json?.api_key;if(typeof apiKey!="string"||!apiKey){let actionUrl=trustedHttpUrl(json?.action_url);throw new Error(`Meta did not issue an API key.${actionUrl?` Complete setup at ${actionUrl}`:""}`)}return{type:"oauth",refresh:identityToken,access:apiKey,expires:Date.now()+API_KEY_LIFETIME_MS}}async function loginMeta(interaction){try{let device=await startDeviceAuthorization(interaction.signal);interaction.notify({type:"device_code",userCode:device.userCode,verificationUri:device.verificationUri,intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds});let identityToken=await pollForIdentityToken(device,interaction.signal);return interaction.notify({type:"progress",message:"Enabling Meta Model API access..."}),await mintApiKey(identityToken,interaction.signal)}catch(e){throw interaction.signal.aborted?new Error("Login cancelled"):e}}var metaOAuth={name:"Meta (Muse subscription)",isSubscription:!0,loginLabel:"Sign in with Meta",login:loginMeta,refresh:(credential,signal)=>mintApiKey(credential.refresh,signal),async toAuth(credential){return{apiKey:credential.access}}};export{metaOAuth};
2
+ var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}function trustedHttpUrl(value){if(typeof value!="string"||!URL.canParse(value))return null;let url=new URL(value);return url.protocol==="https:"||url.protocol==="http:"?url.href:null}var CLIENT_ID="1031625952748946",AUTH_HOST="https://auth.meta.com",DEVICE_AUTHORIZATION_URL=`${AUTH_HOST}/oidc/device/authorization/`,DEVICE_TOKEN_URL=`${AUTH_HOST}/oidc/device/token/`,API_KEY_MINT_URL="https://api.meta.ai/muse-code/key",API_KEY_LIFETIME_MS=1440*60*1e3,REQUEST_TIMEOUT_MS=30*1e3;function requestSignal(signal){return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS),signal])}async function readJson(response){try{let json=await response.json();return json&&typeof json=="object"?json:null}catch{return null}}function errorDetail(json){for(let key of["error_description","detail","message","error"]){let value=json?.[key];if(typeof value=="string"&&value.trim())return`: ${value.trim()}`}return""}function positiveNumber(value){return typeof value=="number"&&Number.isFinite(value)&&value>0?value:void 0}async function startDeviceAuthorization(signal){let response=await fetch(DEVICE_AUTHORIZATION_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({client_id:CLIENT_ID}).toString(),signal:requestSignal(signal)}),json=await readJson(response);if(!response.ok)throw new Error(`Meta device authorization failed with status ${response.status}${errorDetail(json)}`);let deviceCode=json?.device_code,userCode=json?.user_code,verificationUri=trustedHttpUrl(json?.verification_uri_complete)??trustedHttpUrl(json?.verification_uri);if(typeof deviceCode!="string"||!deviceCode||typeof userCode!="string"||!userCode||!verificationUri)throw new Error(`Invalid Meta device authorization response: ${JSON.stringify(json)}`);return{deviceCode,userCode,verificationUri,intervalSeconds:positiveNumber(json?.interval),expiresInSeconds:positiveNumber(json?.expires_in)}}async function pollForIdentityToken(device,signal){return pollOAuthDeviceCodeFlow({intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds,waitBeforeFirstPoll:!0,signal,poll:async()=>{let response=await fetch(DEVICE_TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"urn:ietf:params:oauth:grant-type:device_code",device_code:device.deviceCode,client_id:CLIENT_ID}).toString(),signal:requestSignal(signal)}),json=await readJson(response);if(response.ok&&typeof json?.access_token=="string"&&json.access_token)return{status:"complete",value:json.access_token};switch(json?.error){case"authorization_pending":return{status:"pending"};case"slow_down":return{status:"slow_down",intervalSeconds:positiveNumber(json?.interval)};case"access_denied":return{status:"failed",message:"Meta login was denied."};case"expired_token":return{status:"failed",message:"Meta device authorization expired. Please restart login."};default:return{status:"failed",message:`Meta device token request failed with status ${response.status}${errorDetail(json)}`}}}})}async function mintApiKey(identityToken,signal){let response=await fetch(API_KEY_MINT_URL,{method:"POST",headers:{Accept:"application/json",Authorization:`Bearer ${identityToken}`,"Content-Type":"application/json","x-api-version":"1.0.0"},body:"{}",signal:requestSignal(signal)}),json=await readJson(response);if(response.status===401||response.status===403)throw new Error(`Meta session expired (status ${response.status}). Run \`/login meta\` to sign in again.${errorDetail(json)}`);if(!response.ok)throw new Error(`Meta API key mint failed with status ${response.status}${errorDetail(json)}`);let apiKey=json?.api_key;if(typeof apiKey!="string"||!apiKey){let actionUrl=trustedHttpUrl(json?.action_url);throw new Error(`Meta did not issue an API key.${actionUrl?` Complete setup at ${actionUrl}`:""}`)}return{type:"oauth",refresh:identityToken,access:apiKey,expires:Date.now()+API_KEY_LIFETIME_MS}}async function loginMeta(interaction){try{let device=await startDeviceAuthorization(interaction.signal);interaction.notify({type:"device_code",userCode:device.userCode,verificationUri:device.verificationUri,intervalSeconds:device.intervalSeconds,expiresInSeconds:device.expiresInSeconds});let identityToken=await pollForIdentityToken(device,interaction.signal);return interaction.notify({type:"progress",message:"Enabling Meta Model API access..."}),await mintApiKey(identityToken,interaction.signal)}catch(e){throw interaction.signal.aborted?new Error("Login cancelled"):e}}var metaOAuth={name:"Meta (Muse subscription)",isSubscription:!0,loginLabel:"Sign in with Meta",login:loginMeta,refresh:(credential,signal)=>mintApiKey(credential.refresh,signal),async toAuth(credential){return{apiKey:credential.access}}};export{metaOAuth};
@@ -72,4 +72,4 @@ function normalizeRadiusGatewayUrl(value){return(/^https?:\/\//iu.test(value)?va
72
72
  ${details?`<div class="details">${details}</div>`:""}
73
73
  </main>
74
74
  </body>
75
- </html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading:"Authentication failed",message,details})}function sendPage(response,status,html){response.writeHead(status,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),response.end(html)}async function startOAuthCallbackServer(options){let{providerName,signal}=options;if(signal?.aborted)throw new Error("Login cancelled");let resolveWait=()=>{},rejectWait=()=>{},waitPromise=new Promise((resolve,reject)=>{resolveWait=resolve,rejectWait=reject});waitPromise.catch(()=>{});let claimed=!1,settled=!1,timer,onAbort=()=>finish({error:new Error("Login cancelled")}),finish=result=>{settled||(settled=!0,timer&&clearTimeout(timer),signal?.removeEventListener("abort",onAbort),"error"in result?rejectWait(result.error):resolveWait(result.value))},server=createServer((request,response)=>{(async()=>{let url=new URL(request.url??"/","http://localhost");if(request.method!=="GET"||url.pathname!==options.path){sendPage(response,404,oauthErrorHtml("Callback route not found."));return}if(options.state!==void 0&&url.searchParams.get("state")!==options.state){sendPage(response,400,oauthErrorHtml("State mismatch."));return}if(claimed||settled){sendPage(response,409,oauthErrorHtml("This sign-in has already been handled."));return}let error=url.searchParams.get("error");if(error){let description=url.searchParams.get("error_description")??error;sendPage(response,400,oauthErrorHtml(`${providerName} authorization failed.`,description)),finish({error:new Error(`${providerName} authorization failed: ${description}`)});return}let code=url.searchParams.get("code");if(!code){sendPage(response,400,oauthErrorHtml("Missing authorization code."));return}claimed=!0;try{let value=await options.complete(code);sendPage(response,200,oauthSuccessHtml(`Signed in to ${providerName}. You may now close this page.`)),finish({value})}catch(error2){let failure=error2 instanceof Error?error2:new Error(String(error2));sendPage(response,502,oauthErrorHtml(`${providerName} sign-in failed.`,failure.message)),finish({error:failure})}})()});await new Promise((resolve,reject)=>{server.once("error",reject),server.listen(options.port,options.host,()=>{server.off("error",reject),resolve()})});let address=server.address();if(!address||typeof address=="string")throw server.close(),new Error("OAuth callback server did not bind to TCP");server.on("error",error=>finish({error})),signal?.addEventListener("abort",onAbort,{once:!0}),options.timeoutMs!==void 0&&(timer=setTimeout(()=>finish({error:new Error(`${providerName} sign-in timed out`)}),options.timeoutMs));let redirectHost=options.redirectHost??options.host;return{redirectUri:`http://${redirectHost.includes(":")?`[${redirectHost}]`:redirectHost}:${address.port}${options.path}`,wait:()=>waitPromise,cancel:()=>{claimed||finish({value:void 0})},close:()=>{finish({error:new Error("OAuth callback server closed")}),server.close()}}}var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}function base64urlEncode(bytes){let binary="";for(let byte of bytes)binary+=String.fromCharCode(byte);return btoa(binary).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function generatePKCE(){let verifierBytes=new Uint8Array(32);crypto.getRandomValues(verifierBytes);let verifier=base64urlEncode(verifierBytes),data=new TextEncoder().encode(verifier),hashBuffer=await crypto.subtle.digest("SHA-256",data),challenge=base64urlEncode(new Uint8Array(hashBuffer));return{verifier,challenge}}var CALLBACK_HOST="127.0.0.1",CALLBACK_PORT=1456,CALLBACK_PATH="/oauth/callback",REDIRECT_URI=`http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`,TOKEN_EXPIRY_SKEW_MS=6e4,LOGIN_METHOD_BROWSER="browser",LOGIN_METHOD_DEVICE_CODE="device-code",OAUTH_CLIENT_ID="pi-gateway",OAUTH_SCOPE="gateway offline_access",OAUTH_DEVICE_CODE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:device_code";async function loadRadiusOAuthDiscovery(gateway,signal){let response=await fetch(new URL("/v1/oauth",gateway),{headers:{accept:"application/json"},signal});if(!response.ok)throw new Error(`Could not load Radius OAuth config from ${gateway}: ${response.status} ${await response.text()}`);let discovery=await response.json();if(typeof discovery.authorizationEndpoint!="string")throw new Error(`Invalid Radius OAuth config from ${gateway}`);return{authorizationEndpoint:discovery.authorizationEndpoint}}var OAuthResponseError=class extends Error{status;oauthError;constructor(status,oauthError,description,message){let detail=oauthError?description?`${oauthError}: ${description}`:oauthError:description||String(status);super(`${message}: ${detail}`),this.status=status,this.oauthError=oauthError}};async function readOAuthResponseError(response,message){let text=await response.text().catch(()=>""),oauthError,description;if(text)try{let data=JSON.parse(text);oauthError=typeof data.error=="string"?data.error:void 0,description=typeof data.error_description=="string"?data.error_description:void 0}catch{description=text}return new OAuthResponseError(response.status,oauthError,description,message)}async function requestOAuthToken(gateway,body,signal){let response;try{response=await fetch(new URL("/v1/oauth/token",gateway),{method:"POST",headers:{accept:"application/json","content-type":"application/x-www-form-urlencoded"},body,signal})}catch(error){throw signal.aborted?new Error("Login cancelled"):error}if(!response.ok)throw await readOAuthResponseError(response,"Radius OAuth token request failed");let data=await response.json();return{type:"oauth",access:data.access_token,refresh:data.refresh_token,expires:Date.now()+data.expires_in*1e3-TOKEN_EXPIRY_SKEW_MS,scope:data.scope}}async function loginWithBrowser(gateway,authorizationEndpoint,interaction){let{verifier,challenge}=await generatePKCE(),state=crypto.randomUUID(),authorizeUrl=new URL(authorizationEndpoint);authorizeUrl.search=new URLSearchParams({response_type:"code",client_id:OAUTH_CLIENT_ID,redirect_uri:REDIRECT_URI,scope:OAUTH_SCOPE,code_challenge:challenge,code_challenge_method:"S256",handoff:"url",state}).toString();let callback=await startOAuthCallbackServer({providerName:"Radius",host:CALLBACK_HOST,port:CALLBACK_PORT,path:CALLBACK_PATH,state,complete:code=>requestOAuthToken(gateway,new URLSearchParams({grant_type:"authorization_code",client_id:OAUTH_CLIENT_ID,redirect_uri:REDIRECT_URI,code,code_verifier:verifier}),interaction.signal),signal:interaction.signal});interaction.notify({type:"progress",message:`Listening for OAuth callback on ${REDIRECT_URI}`}),interaction.notify({type:"auth_url",url:authorizeUrl.toString(),instructions:"Continue in your browser."});try{let credential=await callback.wait();if(!credential)throw new Error("OAuth callback did not complete.");return credential}finally{callback.close()}}async function requestDeviceAuthorization(gateway,signal){let response;try{response=await fetch(new URL("/v1/oauth/device",gateway),{method:"POST",headers:{accept:"application/json","content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:OAUTH_CLIENT_ID,scope:OAUTH_SCOPE}),signal})}catch(error){throw signal.aborted?new Error("Login cancelled"):error}if(!response.ok)throw await readOAuthResponseError(response,"Radius OAuth device authorization failed");let data=await response.json();if(!data.device_code||!data.user_code||!data.verification_uri||!data.expires_in)throw new Error("Radius OAuth device authorization response is missing required fields");return{device_code:data.device_code,user_code:data.user_code,verification_uri:data.verification_uri,expires_in:data.expires_in,interval:data.interval}}async function loginWithDeviceCode(gateway,interaction){let device=await requestDeviceAuthorization(gateway,interaction.signal);return interaction.notify({type:"device_code",userCode:device.user_code,verificationUri:device.verification_uri,intervalSeconds:device.interval,expiresInSeconds:device.expires_in}),pollOAuthDeviceCodeFlow({intervalSeconds:device.interval,expiresInSeconds:device.expires_in,signal:interaction.signal,poll:async()=>{try{return{status:"complete",value:await requestOAuthToken(gateway,new URLSearchParams({grant_type:OAUTH_DEVICE_CODE_GRANT_TYPE,client_id:OAUTH_CLIENT_ID,device_code:device.device_code}),interaction.signal)}}catch(error){if(!(error instanceof OAuthResponseError))throw error;switch(error.oauthError){case"authorization_pending":return{status:"pending"};case"slow_down":return{status:"slow_down"};case"expired_token":return{status:"failed",message:"Device authorization expired."};case"access_denied":return{status:"failed",message:"Device authorization was denied."};default:throw error}}}})}function createRadiusOAuth(options){let gateway=normalizeRadiusGatewayUrl(options.gateway);return{name:options.name,async login(interaction){let loginMethod=await interaction.prompt({type:"select",message:`Sign in to ${options.name}:`,options:[{id:LOGIN_METHOD_BROWSER,label:"Sign in with browser (recommended)"},{id:LOGIN_METHOD_DEVICE_CODE,label:"Sign in with device code (when signing in from another device)"}]});if(loginMethod===LOGIN_METHOD_DEVICE_CODE)return loginWithDeviceCode(gateway,interaction);if(loginMethod===LOGIN_METHOD_BROWSER){let discovery=await loadRadiusOAuthDiscovery(gateway,interaction.signal);return loginWithBrowser(gateway,discovery.authorizationEndpoint,interaction)}throw new Error(`Unknown ${options.name} sign-in method: ${loginMethod}`)},async refresh(credential,signal){return await requestOAuthToken(gateway,new URLSearchParams({grant_type:"refresh_token",client_id:OAUTH_CLIENT_ID,refresh_token:credential.refresh}),signal)},async toAuth(credential){return{apiKey:credential.access}}}}export{createRadiusOAuth};
75
+ </html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading:"Authentication failed",message,details})}function sendPage(response,status,html){response.writeHead(status,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),response.end(html)}async function startOAuthCallbackServer(options){let{providerName,signal}=options;if(signal?.aborted)throw new Error("Login cancelled");let resolveWait=()=>{},rejectWait=()=>{},waitPromise=new Promise((resolve,reject)=>{resolveWait=resolve,rejectWait=reject});waitPromise.catch(()=>{});let claimed=!1,settled=!1,timer,onAbort=()=>finish({error:new Error("Login cancelled")}),finish=result=>{settled||(settled=!0,timer&&clearTimeout(timer),signal?.removeEventListener("abort",onAbort),"error"in result?rejectWait(result.error):resolveWait(result.value))},server=createServer((request,response)=>{(async()=>{let url=new URL(request.url??"/","http://localhost");if(request.method!=="GET"||url.pathname!==options.path){sendPage(response,404,oauthErrorHtml("Callback route not found."));return}if(options.state!==void 0&&url.searchParams.get("state")!==options.state){sendPage(response,400,oauthErrorHtml("State mismatch."));return}if(claimed||settled){sendPage(response,409,oauthErrorHtml("This sign-in has already been handled."));return}let error=url.searchParams.get("error");if(error){let description=url.searchParams.get("error_description")??error;sendPage(response,400,oauthErrorHtml(`${providerName} authorization failed.`,description)),finish({error:new Error(`${providerName} authorization failed: ${description}`)});return}let code=url.searchParams.get("code");if(!code){sendPage(response,400,oauthErrorHtml("Missing authorization code."));return}claimed=!0;try{let value=await options.complete(code);sendPage(response,200,oauthSuccessHtml(`Signed in to ${providerName}. You may now close this page.`)),finish({value})}catch(error2){let failure=error2 instanceof Error?error2:new Error(String(error2));sendPage(response,502,oauthErrorHtml(`${providerName} sign-in failed.`,failure.message)),finish({error:failure})}})()});await new Promise((resolve,reject)=>{server.once("error",reject),server.listen(options.port,options.host,()=>{server.off("error",reject),resolve()})});let address=server.address();if(!address||typeof address=="string")throw server.close(),new Error("OAuth callback server did not bind to TCP");server.on("error",error=>finish({error})),signal?.addEventListener("abort",onAbort,{once:!0}),options.timeoutMs!==void 0&&(timer=setTimeout(()=>finish({error:new Error(`${providerName} sign-in timed out`)}),options.timeoutMs));let redirectHost=options.redirectHost??options.host;return{redirectUri:`http://${redirectHost.includes(":")?`[${redirectHost}]`:redirectHost}:${address.port}${options.path}`,wait:()=>waitPromise,cancel:()=>{claimed||finish({value:void 0})},close:()=>{finish({error:new Error("OAuth callback server closed")}),server.close()}}}var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";function abortableSleep(ms,signal,cancelMessage){return new Promise((resolve,reject)=>{if(signal.aborted){reject(new Error(cancelMessage));return}let onAbort=()=>{clearTimeout(timeout),reject(new Error(cancelMessage))},timeout=setTimeout(()=>{signal.removeEventListener("abort",onAbort),resolve()},ms);signal.addEventListener("abort",onAbort,{once:!0})})}async function pollOAuthDeviceCodeFlow(options){let deadline=typeof options.expiresInSeconds=="number"?Date.now()+options.expiresInSeconds*1e3:Number.POSITIVE_INFINITY,intervalMs=Math.max(1e3,Math.floor((options.intervalSeconds??5)*1e3)),slowDownResponses=0;if(options.waitBeforeFirstPoll){let remainingMs=deadline-Date.now();remainingMs>0&&await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}for(;Date.now()<deadline;){if(options.signal.aborted)throw new Error(CANCEL_MESSAGE);let result=await options.poll();if(result.status==="complete")return result.value;if(result.status==="failed")throw new Error(result.message);result.status==="slow_down"&&(slowDownResponses+=1,intervalMs=typeof result.intervalSeconds=="number"&&Number.isFinite(result.intervalSeconds)&&result.intervalSeconds>0?Math.max(1e3,Math.floor(result.intervalSeconds*1e3)):Math.max(1e3,intervalMs+5e3));let remainingMs=deadline-Date.now();if(remainingMs<=0)break;await abortableSleep(Math.min(intervalMs,remainingMs),options.signal,CANCEL_MESSAGE)}throw new Error(slowDownResponses>0?SLOW_DOWN_TIMEOUT_MESSAGE:TIMEOUT_MESSAGE)}function base64urlEncode(bytes){let binary="";for(let byte of bytes)binary+=String.fromCharCode(byte);return btoa(binary).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async function generatePKCE(){let verifierBytes=new Uint8Array(32);crypto.getRandomValues(verifierBytes);let verifier=base64urlEncode(verifierBytes),data=new TextEncoder().encode(verifier),hashBuffer=await crypto.subtle.digest("SHA-256",data),challenge=base64urlEncode(new Uint8Array(hashBuffer));return{verifier,challenge}}function trustedHttpUrl(value){if(typeof value!="string"||!URL.canParse(value))return null;let url=new URL(value);return url.protocol==="https:"||url.protocol==="http:"?url.href:null}var CALLBACK_HOST="127.0.0.1",CALLBACK_PORT=1456,CALLBACK_PATH="/oauth/callback",REDIRECT_URI=`http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`,TOKEN_EXPIRY_SKEW_MS=6e4,LOGIN_METHOD_BROWSER="browser",LOGIN_METHOD_DEVICE_CODE="device-code",OAUTH_CLIENT_ID="pi-gateway",OAUTH_SCOPE="gateway offline_access",OAUTH_DEVICE_CODE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:device_code";async function loadRadiusOAuthDiscovery(gateway,signal){let response=await fetch(new URL("/v1/oauth",gateway),{headers:{accept:"application/json"},signal});if(!response.ok)throw new Error(`Could not load Radius OAuth config from ${gateway}: ${response.status} ${await response.text()}`);let discovery=await response.json(),authorizationEndpoint=trustedHttpUrl(discovery.authorizationEndpoint);if(!authorizationEndpoint)throw new Error(`Invalid Radius OAuth config from ${gateway}`);return{authorizationEndpoint}}var OAuthResponseError=class extends Error{status;oauthError;constructor(status,oauthError,description,message){let detail=oauthError?description?`${oauthError}: ${description}`:oauthError:description||String(status);super(`${message}: ${detail}`),this.status=status,this.oauthError=oauthError}};async function readOAuthResponseError(response,message){let text=await response.text().catch(()=>""),oauthError,description;if(text)try{let data=JSON.parse(text);oauthError=typeof data.error=="string"?data.error:void 0,description=typeof data.error_description=="string"?data.error_description:void 0}catch{description=text}return new OAuthResponseError(response.status,oauthError,description,message)}async function requestOAuthToken(gateway,body,signal){let response;try{response=await fetch(new URL("/v1/oauth/token",gateway),{method:"POST",headers:{accept:"application/json","content-type":"application/x-www-form-urlencoded"},body,signal})}catch(error){throw signal.aborted?new Error("Login cancelled"):error}if(!response.ok)throw await readOAuthResponseError(response,"Radius OAuth token request failed");let data=await response.json();return{type:"oauth",access:data.access_token,refresh:data.refresh_token,expires:Date.now()+data.expires_in*1e3-TOKEN_EXPIRY_SKEW_MS,scope:data.scope}}async function loginWithBrowser(gateway,authorizationEndpoint,interaction){let{verifier,challenge}=await generatePKCE(),state=crypto.randomUUID(),authorizeUrl=new URL(authorizationEndpoint);authorizeUrl.search=new URLSearchParams({response_type:"code",client_id:OAUTH_CLIENT_ID,redirect_uri:REDIRECT_URI,scope:OAUTH_SCOPE,code_challenge:challenge,code_challenge_method:"S256",handoff:"url",state}).toString();let callback=await startOAuthCallbackServer({providerName:"Radius",host:CALLBACK_HOST,port:CALLBACK_PORT,path:CALLBACK_PATH,state,complete:code=>requestOAuthToken(gateway,new URLSearchParams({grant_type:"authorization_code",client_id:OAUTH_CLIENT_ID,redirect_uri:REDIRECT_URI,code,code_verifier:verifier}),interaction.signal),signal:interaction.signal});interaction.notify({type:"progress",message:`Listening for OAuth callback on ${REDIRECT_URI}`}),interaction.notify({type:"auth_url",url:authorizeUrl.toString(),instructions:"Continue in your browser."});try{let credential=await callback.wait();if(!credential)throw new Error("OAuth callback did not complete.");return credential}finally{callback.close()}}async function requestDeviceAuthorization(gateway,signal){let response;try{response=await fetch(new URL("/v1/oauth/device",gateway),{method:"POST",headers:{accept:"application/json","content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:OAUTH_CLIENT_ID,scope:OAUTH_SCOPE}),signal})}catch(error){throw signal.aborted?new Error("Login cancelled"):error}if(!response.ok)throw await readOAuthResponseError(response,"Radius OAuth device authorization failed");let data=await response.json();if(!data.device_code||!data.user_code||!data.verification_uri||!data.expires_in)throw new Error("Radius OAuth device authorization response is missing required fields");let verificationUri=trustedHttpUrl(data.verification_uri);if(!verificationUri)throw new Error("Untrusted verification_uri in Radius OAuth device authorization response");return{device_code:data.device_code,user_code:data.user_code,verification_uri:verificationUri,expires_in:data.expires_in,interval:data.interval}}async function loginWithDeviceCode(gateway,interaction){let device=await requestDeviceAuthorization(gateway,interaction.signal);return interaction.notify({type:"device_code",userCode:device.user_code,verificationUri:device.verification_uri,intervalSeconds:device.interval,expiresInSeconds:device.expires_in}),pollOAuthDeviceCodeFlow({intervalSeconds:device.interval,expiresInSeconds:device.expires_in,signal:interaction.signal,poll:async()=>{try{return{status:"complete",value:await requestOAuthToken(gateway,new URLSearchParams({grant_type:OAUTH_DEVICE_CODE_GRANT_TYPE,client_id:OAUTH_CLIENT_ID,device_code:device.device_code}),interaction.signal)}}catch(error){if(!(error instanceof OAuthResponseError))throw error;switch(error.oauthError){case"authorization_pending":return{status:"pending"};case"slow_down":return{status:"slow_down"};case"expired_token":return{status:"failed",message:"Device authorization expired."};case"access_denied":return{status:"failed",message:"Device authorization was denied."};default:throw error}}}})}function createRadiusOAuth(options){let gateway=normalizeRadiusGatewayUrl(options.gateway);return{name:options.name,async login(interaction){let loginMethod=await interaction.prompt({type:"select",message:`Sign in to ${options.name}:`,options:[{id:LOGIN_METHOD_BROWSER,label:"Sign in with browser (recommended)"},{id:LOGIN_METHOD_DEVICE_CODE,label:"Sign in with device code (when signing in from another device)"}]});if(loginMethod===LOGIN_METHOD_DEVICE_CODE)return loginWithDeviceCode(gateway,interaction);if(loginMethod===LOGIN_METHOD_BROWSER){let discovery=await loadRadiusOAuthDiscovery(gateway,interaction.signal);return loginWithBrowser(gateway,discovery.authorizationEndpoint,interaction)}throw new Error(`Unknown ${options.name} sign-in method: ${loginMethod}`)},async refresh(credential,signal){return await requestOAuthToken(gateway,new URLSearchParams({grant_type:"refresh_token",client_id:OAUTH_CLIENT_ID,refresh_token:credential.refresh}),signal)},async toAuth(credential){return{apiKey:credential.access}}}}export{createRadiusOAuth};
@@ -294,7 +294,7 @@ For decision questions, start with Jev: it has the lower [usage rate](https://bk
294
294
 
295
295
  - **Versions.** `jev-latest` and versioned IDs are accepted but always select the current Jev; you cannot pin a version. Log the revision each answer reports, and re-check tuned thresholds when it changes.
296
296
  - **Language.** English is Jev's primary training language; other languages work with lower accuracy.
297
- - **Images.** An experimental TypeSafe feature, so accuracy and limits can change. Send PNG or JPEG images between 280 and 1568 pixels per side as base64 `assets` in a JSON request; TypeSafe's SDKs upload images in a form Bkper AI does not accept. Image tokens count as input. See TypeSafe's [Images](https://docs.typesafe.ai/images) guide.
297
+ - **Images.** An experimental TypeSafe feature, so accuracy and limits can change. Send PNG or JPEG images between 280 and 1568 pixels per side, with TypeSafe's SDKs or as base64 `assets` in a JSON request. Image tokens count as input. See TypeSafe's [Images](https://docs.typesafe.ai/images) guide.
298
298
  - **Known weak spots.** Literal reading, arithmetic, date comparison, and large states full of irrelevant detail. See [Jev 1.13 jaggedness](https://docs.typesafe.ai/model-jaggedness/jev-1.13).
299
299
 
300
300
  ### Clef
@@ -322,7 +322,7 @@ For decision questions, start with Jev: it has the lower [usage rate](https://bk
322
322
  - [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai.md#ask-a-system-one-model) — call System One models from an app Worker, a script, or a browser.
323
323
  - [AI Gateway API: decision questions](https://bkper.com/docs/api/ai-gateway.md#decision-questions) — the request, answer guarantees, errors, and compatibility with TypeSafe.
324
324
  - [AI Gateway API: extraction questions](https://bkper.com/docs/api/ai-gateway.md#extraction-questions) — question settings, limits, and answer guarantees.
325
- - [Open System One](https://github.com/bkper/open-systemone) — the open specification behind `POST /v1/systemone`: question types, answer guarantees, and errors.
325
+ - [Open System One](https://github.com/opensystemoneorg/opensystemone) — the open specification behind `POST /v1/systemone`: question types, answer guarantees, and errors.
326
326
  - [Models and Usage](https://bkper.com/docs/ai/models.md) — usage rates, the allowance, and privacy.
327
327
  - Building with a coding agent? Point it to this page for design, and to `https://bkper.com/docs/api/ai-gateway.md` for the endpoint, authentication, and errors. TypeSafe's [agent skill](https://docs.typesafe.ai/agent-skill) adds questions and patterns for decision questions.
328
328
  - TypeSafe: [System One](https://docs.typesafe.ai/concepts/system-one), [How to build with System One](https://docs.typesafe.ai/concepts/how-to-build-with-system-one), [State](https://docs.typesafe.ai/concepts/state), [Confidence](https://docs.typesafe.ai/confidence), and [Patterns](https://docs.typesafe.ai/patterns).
@@ -175,7 +175,8 @@ curl --fail-with-body https://ai.bkper.app/v1/systemone \
175
175
  - **`choice`**: `criteria` maps 1–255 options to descriptions, which may be `null`.
176
176
  - **`score`**: `criteria` lists 2–10 levels, lowest first.
177
177
  - The body accepts only `model`, `state`, and `questions`, plus image `assets` on models with image input. `reference_date` applies only to extraction questions; `jev` and `clef` reject it with `unsupported_parameter`. Responses fields such as `input`, `stream`, `store`, `temperature`, and `metadata` are rejected.
178
- - **`clef`** answers at most 64 questions, named with up to 100 letters, digits, `_`, `.`, or `-`, and needs at least 2 choice options. It reads up to 4 image `assets` (4 MiB each, 8 MiB in total) before the state, in `assets` order. Requests beyond these limits return `unsupported_parameter`; input over its 65,536-token context returns `provider_rejected`.
178
+ - Questions without `instructions` are asked by their name on models that need instructions: noul questions on `jev`, and every question on `clef`.
179
+ - **`clef`** answers at most 64 questions, named with up to 100 letters, digits, `_`, `.`, or `-`, and needs at least 2 choice options and at least 2 score levels. It reads up to 4 image `assets` (4 MiB each, 8 MiB in total) before the state, in `assets` order. Requests beyond these limits return `unsupported_parameter`; input over its 65,536-token context returns `provider_rejected`.
179
180
 
180
181
  The model evaluates each question independently and in parallel against the same state.
181
182
 
@@ -40,7 +40,7 @@ In both cases usage counts against the user's allowance. See [Call from a browse
40
40
 
41
41
  Every System One question, decision or extraction, goes to `POST /v1/systemone`. There are two ways to send it:
42
42
 
43
- - **TypeSafe's SDK**, `@typesafe-ai/sdk`, types decision questions and their answers for you. It does not send extraction questions or images.
43
+ - **TypeSafe's SDK**, `@typesafe-ai/sdk`, types decision questions and their answers for you, and uploads images. It does not send extraction questions.
44
44
  - **`fetch`** sends any question type, and images.
45
45
 
46
46
  Bkper AI validates every answer against its question before responding, so your code can use it directly. Keep the questions, the thresholds, and the function that turns answers and facts into an action in one module.
@@ -0,0 +1,33 @@
1
+ # Bank Connections
2
+
3
+ Use this guide when a user wants to connect a bank, credit card, or other financial institution to a Book.
4
+
5
+ Bank connections are set up by the user in the Bkper UI, not through the CLI. Your job is to send them straight to the connection flow for their country.
6
+
7
+ ## Route
8
+
9
+ 1. Know the Book. If the Book ID is unknown, ask which Book or find it with `bkper book list`.
10
+ 2. Ask which country the bank is in, and the bank's name if the user has not said it.
11
+ 3. Check the live coverage. Do not rely on memory or a fixed list:
12
+
13
+ ```bash
14
+ curl -s https://bkper.app/automations/api/banks/coverage
15
+ ```
16
+
17
+ Each entry has the country `code` and `name`.
18
+
19
+ 4. If the country is supported, share the link with the Book ID, the lowercase country code, and the encoded bank name:
20
+
21
+ ```text
22
+ https://bkper.app/automations/{bookId}/banks/{countryCode}?search={bankName}
23
+ ```
24
+
25
+ Leave out `?search=` if the bank name is unknown.
26
+
27
+ The link opens the connection flow for that country, where the user picks their bank and signs in.
28
+
29
+ 5. If the country is not supported, say so and suggest importing bank statements instead.
30
+
31
+ ## After connecting
32
+
33
+ Once connected, the user links each bank account to an Account in the Book from User Connections: `https://bkper.app/automations/{bookId}/connections`. Transactions can take up to 48 hours to appear.
@@ -11,11 +11,17 @@ When asking a user to review or confirm Bkper data, include a browser link when
11
11
  Use canonical Bkper links:
12
12
 
13
13
  - Book transactions: `https://bkper.app/books/{bookId}/transactions`
14
+ - Book accounts: `https://bkper.app/books/{bookId}/accounts`
14
15
  - Transaction: `https://bkper.app/books/{bookId}/transactions/{transactionId}`
15
- - Accounts: `https://bkper.app/books/{bookId}/accounts`
16
- - Account: `https://bkper.app/books/{bookId}/accounts/{accountId}`
17
16
  - Query view: `https://bkper.app/books/{bookId}/transactions?query={encodedQuery}`
18
17
  - Chart/report view: `https://bkper.app/books/{bookId}/transactions?query={encodedQuery}&charts=true`
18
+ - Banks: `https://bkper.app/automations/{bookId}/banks`
19
+ - Apps: `https://bkper.app/automations/{bookId}/apps`
20
+ - User Connections: `https://bkper.app/automations/{bookId}/connections`
21
+ - User Connection: `https://bkper.app/automations/{bookId}/connections/{connectionId}`
22
+ - Book Integrations: `https://bkper.app/automations/{bookId}/integrations`
23
+ - Book Integration: `https://bkper.app/automations/{bookId}/integrations/{integrationId}`
24
+ - AI usage: `https://ai.bkper.app/`
19
25
 
20
26
  Encode query parameter values. Only build links from IDs and queries obtained from Bkper or provided by the user; do not invent IDs.
21
27
 
package/lib/docs/index.md CHANGED
@@ -5,6 +5,7 @@ Reference docs for Bkper tasks. Load only the specific doc(s) relevant to the ta
5
5
  For Bkper app implementation, refactoring, or code review, always read `apps/quality.md` alongside the task-specific references. After implementation, review the changed code against the guidelines before considering the work complete.
6
6
 
7
7
  - `cli/data-management.md` — CLI reference for managing financial data and files: books, accounts, groups, files, transactions (list, count), events and bot responses (bot errors, replay, delete), per-account balance queries, query operators (on:, after:, before:, account:, group:), JSON output shapes and jq reshaping, human-review Bkper UI links, batch operations via stdin/piping, collections.
8
+ - `automations/bank-connections.md` — Guiding a user to connect a bank to a Book: ask the country, check live coverage, share the link that opens the connection flow, and link bank accounts to Book Accounts afterwards.
8
9
  - `cli/app-usage.md` — Using Bkper apps to do work on a Book instead of rebuilding what they do: list a Book's installed apps and the app catalog, read descriptions, readmes, and property schemas, match them to the Book's properties, read an app's OpenAPI spec and call its API as the user, install apps.
9
10
  - `cli/app-management.md` — CLI reference for building and deploying Bkper apps: init/git clone/credential helpers, dev/build/deploy workflow, app install/uninstall, secrets management, app logs, bkper.yaml configuration reference (identity, branding, events, menu integration, deployment).
10
11
  - `apps/overview.md` — Platform evaluation and capability overview: use when comparing managed Bkper hosting with self-managed infrastructure or clarifying platform responsibilities; use the task-specific app references for implementation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bkper",
3
- "version": "5.8.2",
3
+ "version": "5.8.3",
4
4
  "description": "Official Bkper CLI for accounting data, automation, apps, and AI agents",
5
5
  "bin": {
6
6
  "bkper": "lib/cli.js"
@@ -64,13 +64,14 @@
64
64
  "upgrade:api": "bun update @bkper/bkper-api-types --latest && bun update bkper-js --latest"
65
65
  },
66
66
  "dependencies": {
67
- "@earendil-works/pi-agent-core": "1.4.0",
68
- "@earendil-works/pi-ai": "1.4.0",
69
- "@earendil-works/pi-codemode": "1.4.0",
70
- "@earendil-works/pi-coding-agent": "1.4.0",
71
- "@earendil-works/pi-mcp": "1.4.0",
72
- "@earendil-works/pi-telemetry": "1.4.0",
73
- "@earendil-works/pi-tui": "1.4.0",
67
+ "@earendil-works/pi-agent-core": "1.5.1",
68
+ "@earendil-works/pi-ai": "1.5.1",
69
+ "@earendil-works/pi-codemode": "1.5.1",
70
+ "@earendil-works/pi-coding-agent": "1.5.1",
71
+ "@earendil-works/pi-mcp": "1.5.1",
72
+ "@earendil-works/pi-schema": "1.5.1",
73
+ "@earendil-works/pi-telemetry": "1.5.1",
74
+ "@earendil-works/pi-tui": "1.5.1",
74
75
  "@silvia-odwyer/photon-node": "0.3.4",
75
76
  "bkper-js": "^2.45.1",
76
77
  "commander": "^13.1.0",