server/wrapInjected.mjs

import get from 'lodash-es/get.js'
import isestr from 'wsemi/src/isestr.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isfun from 'wsemi/src/isfun.mjs'


//wrapInjected: 部署方注入函數(getUserByToken / verifyClientUser / verifyAppUser)之包裝(W 契約, spec/設計要點與取捨.md ADR-023)
//why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
//部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址(修正前探測實證)。
//包裝後失敗視同否定結果(由呼叫端給 failValue), 走各呼叫點既有之查無 / 無權限分支回自家 key; 上游原文不回前端、不入 log。


//keysUpstream: 上游失敗可照記之 key 白名單 = w-web-sso 對外 helper 之 K 契約 14 個 key(w-web-sso ADR-068)
//與 sso 之同步點: sso 之 K 契約增減 key 時須同步本表(見 CLAUDE_rulebook.md「權杖不外洩」)
let keysUpstream = [
    'invalidUrl',
    'invalidTokenSelf',
    'invalidTokenTar',
    'invalidUserIdTar',
    'noTokenKeyValueInUrl',
    'noTokenKeyUserIdInUrl',
    'noTokenInUrl',
    'cannotGetUserByUrl',
    'cannotGetUsersByUrl',
    'cannotGetUserDataByUrl',
    'cannotGetUsersDataByUrl',
    'noUserDataByUrl',
    'noUsersDataByUrl',
    'noUserDataAfterConvert',
]


//reName: Error.name 之名稱形狀(同 w-web-sso helper 診斷之 errName), 不符者不記(name 亦可被設為任意字串)
let reName = /^[A-Za-z]{1,40}$/


/**
 * 描述上游失敗, 供寫入 log: 絕不含上游原文
 * - reject 值(字串)或其 message 屬白名單 key → { upstream: key }
 * - 其餘 → { upstreamType, upstreamName }: 型別(null / array / typeof)與名稱形狀之 Error.name(無則 '')
 *
 * @param {*} err 輸入注入函數拋出或 reject 之值
 * @returns {Object} 回傳描述物件
 */
function describeUpstream(err) {
    let key = isestr(err) ? err : get(err, 'message')
    if (isestr(key) && keysUpstream.includes(key)) {
        return { upstream: key }
    }
    let upstreamType = (err === null) ? 'null' : (isarr(err) ? 'array' : typeof err)
    let name = get(err, 'name')
    let upstreamName = (isestr(name) && reName.test(name)) ? name : ''
    return { upstreamType, upstreamName }
}


/**
 * 包裝注入函數: 回傳(同步或 Promise)原樣回傳; 同步拋錯或 reject 回 failValue, 並以 describeUpstream(err) 呼叫 onFail
 * 包裝後一律回 Promise(呼叫端既有之 ispm 判斷照常)
 *
 * @param {Function} fn 輸入注入函數
 * @param {Object} [opt={}] 輸入設定物件
 * @param {*} [opt.failValue=null] 輸入失敗時之回傳值(getUserByToken 為 null 等同查無, verify* 為 false 等同無權限)
 * @param {Function} [opt.onFail=null] 輸入失敗時之紀錄函數, 只收到 describeUpstream 之結果(收不到上游原文); 其自身拋錯不影響回傳
 * @returns {Function} 回傳包裝後之函數
 */
function wrapInjected(fn, opt = {}) {
    let failValue = get(opt, 'failValue', null)
    let onFail = get(opt, 'onFail', null)
    return async (...args) => {
        try {
            return await fn(...args) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截
        }
        catch (err) {
            if (isfun(onFail)) {
                try {
                    onFail(describeUpstream(err))
                }
                catch (e) {} //紀錄失敗不改變「失敗=否定結果」之語意
            }
            return failValue
        }
    }
}


export {
    keysUpstream,
    describeUpstream
}
export default wrapInjected