w-web-perm 1.1.4 → 1.1.6

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/css/app.6918ec45.css +1 -0
  3. package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.3c580e72.css} +1 -1
  4. package/dist/index.tmp +2 -2
  5. package/dist/js/app-legacy.5f15a58f.js +1 -0
  6. package/dist/js/app.5f15a58f.js +1 -0
  7. package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.38f515f9.js} +16 -16
  8. package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.38f515f9.js} +16 -16
  9. package/dist/w-web-perm.umd.js +2 -2
  10. package/dist/w-web-perm.umd.js.map +1 -1
  11. package/docs/WWebPerm.html +7 -7
  12. package/docs/global.html +2068 -111
  13. package/docs/index.html +2 -2
  14. package/docs/server_WWebPerm.mjs.html +63 -31
  15. package/docs/server_lockSave.mjs.html +115 -0
  16. package/docs/server_maskLog.mjs.html +176 -0
  17. package/docs/server_procCore.mjs.html +31 -5
  18. package/docs/server_wrapInjected.mjs.html +163 -0
  19. package/docs/src_fetchJson.mjs.html +31 -5
  20. package/docs/src_js_legendFit.mjs.html +225 -0
  21. package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
  22. package/docs/src_plugins_submitGuard.mjs.html +163 -0
  23. package/package.json +13 -13
  24. package/public/index.html +2 -0
  25. package/server/WWebPerm.mjs +61 -29
  26. package/server/lockSave.mjs +43 -0
  27. package/server/maskLog.mjs +104 -0
  28. package/server/procCore.mjs +29 -3
  29. package/server/procLang.mjs +8 -0
  30. package/server/procStaInfor.mjs +32 -14
  31. package/server/wrapInjected.mjs +91 -0
  32. package/src/components/Common/CheckYes.vue +6 -0
  33. package/src/components/Common/CheckYesNo.vue +6 -0
  34. package/src/components/LayoutContentGrups.vue +30 -21
  35. package/src/components/LayoutContentPemis.vue +30 -21
  36. package/src/components/LayoutContentStaInfor.vue +74 -5
  37. package/src/components/LayoutContentTargets.vue +36 -22
  38. package/src/components/LayoutContentUsers.vue +31 -21
  39. package/src/components/RelationChips.vue +11 -31
  40. package/src/components/VeCgrups.vue +8 -1
  41. package/src/components/VeCpemis.vue +8 -1
  42. package/src/components/VeCrules.vue +8 -1
  43. package/src/components/VeGrupBlngUsers.vue +37 -31
  44. package/src/components/VePemiBlngGrups.vue +37 -31
  45. package/src/fetchJson.mjs +28 -2
  46. package/src/getPerm.mjs +9 -6
  47. package/src/getPermUserInfor.mjs +12 -9
  48. package/src/js/legendFit.mjs +153 -0
  49. package/src/plugins/mUI.mjs +20 -0
  50. package/src/plugins/reuseDeletedIds.mjs +64 -0
  51. package/src/plugins/submitGuard.mjs +91 -0
  52. package/srv.mjs +20 -1
  53. package/dist/css/app.506ce4d8.css +0 -1
  54. package/dist/js/app-legacy.8e615442.js +0 -1
  55. package/dist/js/app.8e615442.js +0 -1
  56. package/tableTags-web-perm.json +0 -1
package/docs/index.html CHANGED
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 13:05:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -70,14 +70,18 @@ import replace from 'wsemi/src/replace.mjs'
70
70
  import strdelleft from 'wsemi/src/strdelleft.mjs'
71
71
  import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
72
72
  import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
73
+ import cacheSt from 'wsemi/src/cacheSt.mjs'
73
74
  import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
74
75
  import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
75
76
  import ds from '../src/schema/index.mjs'
76
77
  import { getUserRules } from '../src/plugins/mShare.mjs'
77
78
  import procCore from './procCore.mjs'
79
+ import createLockSave from './lockSave.mjs'
78
80
  import procLang from './procLang.mjs'
79
81
  import srLogInit from './srLog.mjs'
80
82
  import procStaInfor from './procStaInfor.mjs'
83
+ import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
84
+ import wrapInjected from './wrapInjected.mjs'
81
85
 
82
86
 
83
87
  /**
@@ -87,9 +91,9 @@ import procStaInfor from './procStaInfor.mjs'
87
91
  * @param {Function} WOrm 輸入資料庫ORM函數
88
92
  * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
89
93
  * @param {String} db 輸入資料庫名稱字串
90
- * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫
91
- * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數)
92
- * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
94
+ * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
95
+ * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數);函數拋錯或reject時視同回傳false
96
+ * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
93
97
  * @param {Object} [opt={}] 輸入設定物件,預設{}
94
98
  * @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006
95
99
  * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
@@ -157,7 +161,7 @@ import procStaInfor from './procStaInfor.mjs'
157
161
  * isAdmin: 'y',
158
162
  * }
159
163
  * }
160
- * console.log('invalid token', token)
164
+ * console.log('invalid token') //勿印權杖值
161
165
  * console.log('於生產環境時得加入SSO等驗證token機制')
162
166
  * return {}
163
167
  * }
@@ -321,6 +325,27 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
321
325
  let srLog = srLogInit(opt)
322
326
 
323
327
 
328
+ //注入函數一律經包裝(W 契約, spec/設計要點與取捨.md ADR-023), 以包裝版取代三個參數
329
+ //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
330
+ //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址.
331
+ //包裝後失敗視同否定結果(getUserByToken → null 等同查無, verify* → false 等同無權限), 走各呼叫點既有分支回自家 key;
332
+ //上游原文不回前端、不入 log, 只於此記 warn(白名單 key, 或型別與 Error.name). 同一次失敗 log 2~3 筆: 此處 warn(因) + 呼叫點 error(果).
333
+ //以下取代後, 本函數內除此三行外不得再直接使用原始參數(盤點見 CLAUDE_rulebook.md「權杖不外洩」, 測試 unit-wrapInjected WRAP-SITE-*)
334
+ let logInjectFail = (name) => {
335
+ return (d) => {
336
+ srLog.warn({ event: `inject-${name}-fail`, ...d })
337
+ }
338
+ }
339
+ getUserByToken = wrapInjected(getUserByToken, { failValue: null, onFail: logInjectFail('getUserByToken') })
340
+ verifyClientUser = wrapInjected(verifyClientUser, { failValue: false, onFail: logInjectFail('verifyClientUser') })
341
+ verifyAppUser = wrapInjected(verifyAppUser, { failValue: false, onFail: logInjectFail('verifyAppUser') })
342
+ //getUserById: 第四個注入函數(選填; useCheckUser=true 時由 WServOrm 於 execute 內呼叫, 原 reject 原文經 pm2resolve 回前端), 有給才包裝;
343
+ //失敗 → null 等同查無, 由 WServOrm 既有之查無分支拒絕(fail-closed). 須於 WServOrm 建構前取代(建構時即取走此函數)
344
+ if (isfun(getUserById)) {
345
+ getUserById = wrapInjected(getUserById, { failValue: null, onFail: logInjectFail('getUserById') })
346
+ }
347
+
348
+
324
349
  //WServOrm
325
350
  let optWServOrm = {
326
351
  useCheckUser,
@@ -344,8 +369,14 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
344
369
  let kmx = pmKeyMutex()
345
370
 
346
371
 
372
+ //lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
373
+ //同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在 kmx 之外層, 不同操作者之並行仍由 kmx 序列化(ADR-012)
374
+ let cst = cacheSt()
375
+ let lockSave = createLockSave(cst)
376
+
377
+
347
378
  //procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
348
- let pc = procCore(woItems, procOrm, { srLog, kmx })
379
+ let pc = procCore(woItems, procOrm, { srLog, kmx, lockSave })
349
380
 
350
381
 
351
382
  //procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
@@ -443,7 +474,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
443
474
 
444
475
  //check
445
476
  if (!iseobj(userSelf)) {
446
- console.log(`token`, token)
477
+ console.log(`token`, maskTok(token))
447
478
  srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
448
479
  return Promise.reject(`cannotFindUserFromToken`)
449
480
  }
@@ -525,7 +556,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
525
556
 
526
557
  //check
527
558
  if (!iseobj(user)) {
528
- console.log(`token`, token)
559
+ console.log(`token`, maskTok(token))
529
560
  srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
530
561
  return Promise.reject(`cannotFindUserFromToken`)
531
562
  }
@@ -797,8 +828,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
797
828
 
798
829
  //check
799
830
  if (!isestr(token)) {
800
- console.log('req.query', get(req, 'query'))
801
- console.log('token', token)
831
+ console.log('req.query', maskQuery(get(req, 'query')))
832
+ console.log('token', maskTok(token))
802
833
  console.log('[API]getUserByToken/check token: invalid token')
803
834
  srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
804
835
  return Promise.reject(`tokenNoPermission`)
@@ -810,7 +841,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
810
841
 
811
842
  //check
812
843
  if (!iseobj(user)) {
813
- console.log('token', token)
844
+ console.log('token', maskTok(token))
814
845
  console.log('[API]getUserByToken/check user: invalid user')
815
846
  srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
816
847
  return Promise.reject(`tokenNoPermission`)
@@ -847,8 +878,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
847
878
 
848
879
  //check
849
880
  if (!isestr(token)) {
850
- console.log('req.query', get(req, 'query'))
851
- console.log('token', token)
881
+ console.log('req.query', maskQuery(get(req, 'query')))
882
+ console.log('token', maskTok(token))
852
883
  console.log('[API]getPerm/check token: invalid token')
853
884
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
854
885
  return Promise.reject(`tokenNoPermission`)
@@ -860,7 +891,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
860
891
 
861
892
  //check
862
893
  if (!iseobj(userSelf)) {
863
- console.log('token', token)
894
+ console.log('token', maskTok(token))
864
895
  console.log('[API]getPerm/check userSelf: invalid userSelf')
865
896
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
866
897
  return Promise.reject(`tokenNoPermission`)
@@ -872,7 +903,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
872
903
 
873
904
  //check
874
905
  if (!isestr(userIdSelf)) {
875
- console.log('token', token)
906
+ console.log('token', maskTok(token))
876
907
  console.log('userSelf', userSelf)
877
908
  console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
878
909
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
@@ -885,7 +916,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
885
916
 
886
917
  //check
887
918
  if (!iseobj(userWithRulesSelf)) {
888
- console.log('token', token)
919
+ console.log('token', maskTok(token))
889
920
  console.log('userSelf', userSelf)
890
921
  console.log('userIdSelf', userIdSelf)
891
922
  console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
@@ -924,7 +955,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
924
955
 
925
956
  //check
926
957
  if (!isestr(tokenSelf)) {
927
- console.log('req.query', get(req, 'query'))
958
+ console.log('req.query', maskQuery(get(req, 'query')))
928
959
  console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
929
960
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
930
961
  return Promise.reject(`tokenNoPermission`)
@@ -936,8 +967,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
936
967
 
937
968
  //check
938
969
  if (!iseobj(userSelf)) {
939
- console.log('req.query', get(req, 'query'))
940
- console.log('tokenSelf', tokenSelf)
970
+ console.log('req.query', maskQuery(get(req, 'query')))
971
+ console.log('tokenSelf', maskTok(tokenSelf))
941
972
  console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
942
973
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
943
974
  return Promise.reject(`tokenNoPermission`)
@@ -949,8 +980,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
949
980
 
950
981
  //check
951
982
  if (!isestr(userIdFind)) {
952
- console.log('req.query', get(req, 'query'))
953
- console.log('tokenSelf', tokenSelf)
983
+ console.log('req.query', maskQuery(get(req, 'query')))
984
+ console.log('tokenSelf', maskTok(tokenSelf))
954
985
  console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
955
986
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
956
987
  return Promise.reject(`tokenNoPermission`)
@@ -999,8 +1030,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
999
1030
 
1000
1031
  //check
1001
1032
  if (!isestr(token)) {
1002
- console.log('req.query', get(req, 'query'))
1003
- console.log('token', token)
1033
+ console.log('req.query', maskQuery(get(req, 'query')))
1034
+ console.log('token', maskTok(token))
1004
1035
  console.log('[API]syncAndReplaceTabs/check token: invalid token')
1005
1036
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1006
1037
  return Promise.reject(`tokenNoPermission`)
@@ -1011,7 +1042,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1011
1042
 
1012
1043
  //check
1013
1044
  if (!isestr(keyTable)) {
1014
- console.log('req.query', get(req, 'query'))
1045
+ console.log('req.query', maskQuery(get(req, 'query')))
1015
1046
  console.log('keyTable', keyTable)
1016
1047
  console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
1017
1048
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
@@ -1031,7 +1062,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1031
1062
 
1032
1063
  //check user
1033
1064
  if (!iseobj(user)) {
1034
- console.log('token', token)
1065
+ console.log('token', maskTok(token))
1035
1066
  console.log('[API]syncAndReplaceTabs/check user: invalid user')
1036
1067
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1037
1068
  return Promise.reject(`tokenNoPermission`)
@@ -1042,7 +1073,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1042
1073
 
1043
1074
  //check userId
1044
1075
  if (!isestr(userId)) {
1045
- console.log('token', token)
1076
+ console.log('token', maskTok(token))
1046
1077
  console.log('user', user)
1047
1078
  console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
1048
1079
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
@@ -1099,8 +1130,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1099
1130
  //check
1100
1131
  if (!isestr(token)) {
1101
1132
  console.log('apiType', apiType)
1102
- console.log('authorization', authorization)
1133
+ console.log('authorization', maskTok(authorization))
1103
1134
  console.log('invalid token')
1135
+ srLog.error({ event: 'verifyConn-error', ok: false, apiType, msg: 'tokenNoPermission' }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」; 同 w-web-api、w-web-task); 只記 key, 不記 authorization 原值
1104
1136
  return false
1105
1137
  }
1106
1138
 
@@ -1125,8 +1157,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1125
1157
  //origin
1126
1158
  let origin = get(headers, 'origin', '')
1127
1159
 
1128
- //referer
1129
- let referer = get(headers, 'referer', '')
1160
+ //referer, 只記 origin + pathname(ADR-023): 前端以 ?token= 開站(w-ui-loginout 讀完不清網址), 同源請求之 Referer 預設帶完整網址, 原樣記錄即把使用者權杖寫入 srLog
1161
+ let referer = maskUrl(get(headers, 'referer', ''))
1130
1162
 
1131
1163
  //info, 失敗時 msg 為 err key(cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等), 供資安稽核與統計
1132
1164
  if (b) {
@@ -1143,7 +1175,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1143
1175
  let user = await getUserByToken(token)
1144
1176
  let userId = get(user, 'id', '')
1145
1177
  if (!isestr(userId)) {
1146
- console.log('token', token)
1178
+ console.log('token', maskTok(token))
1147
1179
  console.log('userId', userId)
1148
1180
  srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
1149
1181
  return Promise.reject(`cannotFindUserId`)
@@ -1221,7 +1253,7 @@ export default WWebPerm
1221
1253
  <br class="clear">
1222
1254
 
1223
1255
  <footer>
1224
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 13:05:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1256
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1225
1257
  </footer>
1226
1258
 
1227
1259
  <script>prettyPrint();</script>
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>server/lockSave.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">server/lockSave.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+
51
+
52
+ /**
53
+ * 有副作用之寫入路徑(如清單儲存 updateUsersList / updateTokensList / updateIpsList)之雙擊防護(後端, ADR-025)
54
+ *
55
+ * 以「操作:操作者 id」於 wsemi cacheSt 原子占位執行 fn, 同一操作者之同一操作處理中再送出即 reject 錯誤 key
56
+ * (預設 'saveInProgress', 可以 opt.errKey 指定; 皆為 i18n key, 見 procLang); 不同操作者、不同操作互不影響;
57
+ * fn 之 reject 原樣 bubble; fn 結束(含失敗)即釋放.
58
+ * 第 2 次不排隊執行: 其內容與第 1 次相同, 排隊後再寫一次會把第 1 次剛建立之列當修改覆寫
59
+ * (依序重送另由各專案之資料層檢查擋, 如 SSO procCore.updateTabItems 之新增列檢查回 'saveNewRowExists').
60
+ * operatorId 可為操作者 id 或「操作者 id + 列識別」(只擋同一列之連發, 同批其他列不受影響).
61
+ *
62
+ * @param {Object} cst 輸入 wsemi cacheSt 實例
63
+ * @returns {Function} 回傳 async (op, operatorId, fn, opt = {}) => fn 之結果, opt.errKey 為占位衝突時 reject 之 key
64
+ */
65
+ function createLockSave(cst) {
66
+
67
+ let lockSave = async (op, operatorId, fn, opt = {}) => {
68
+
69
+ //errKey
70
+ let errKey = get(opt, 'errKey', '')
71
+ if (!isestr(errKey)) {
72
+ errKey = 'saveInProgress'
73
+ }
74
+
75
+ let key = `${op}:${operatorId}`
76
+ return await cst.setWithFree([key], fn)
77
+ .catch((err) => {
78
+ //remap setWithFree 之占位衝突 → i18n key; fn 內之 reject 原樣 bubble
79
+ if (err === `setWithFree: key in use: ${key}`) {
80
+ return Promise.reject(errKey)
81
+ }
82
+ return Promise.reject(err)
83
+ })
84
+ }
85
+
86
+ return lockSave
87
+ }
88
+
89
+
90
+ export default createLockSave
91
+ </code></pre>
92
+ </article>
93
+ </section>
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <br class="clear">
103
+
104
+ <footer>
105
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
106
+ </footer>
107
+
108
+ <script>prettyPrint();</script>
109
+ <script src="scripts/polyfill.js"></script>
110
+ <script src="scripts/linenumber.js"></script>
111
+
112
+
113
+
114
+ </body>
115
+ </html>
@@ -0,0 +1,176 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>server/maskLog.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">server/maskLog.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import isestr from 'wsemi/src/isestr.mjs'
49
+ import isarr from 'wsemi/src/isarr.mjs'
50
+
51
+
52
+ //maskLog: 權杖與網址寫入 console / srLog 前之遮罩(M 契約, spec/設計要點與取捨.md ADR-023)
53
+ //四 repo 一致(w-web-sso server/srLog.mjs 之 maskToken 與 src/maskUrl.mjs、w-web-api / w-web-task 之 maskTok), 以同一組測資守護(test/unit-maskLog.test.mjs)
54
+ //why: 原 console 站點原樣印出 token / req.query / authorization(Hapi 對重複之 query 參數給陣列, 型別檢查失敗之分支正是印出處), verifyConn 之 srLog 原樣記 referer
55
+
56
+
57
+ //safe: 露出字元之控制字元換成 '?', 避免請求端以 CR/LF 偽造 log 行
58
+ function safe(s) {
59
+ return Array.from(s, (c) => {
60
+ let x = c.charCodeAt(0)
61
+ return (x &lt; 32 || x === 127) ? '?' : c
62
+ }).join('')
63
+ }
64
+
65
+
66
+ /**
67
+ * 權杖遮罩
68
+ * - 空值(undefined / null / '')回 ''
69
+ * - 陣列逐元素遮罩
70
+ * - 字串(長度 n)露出前後各 k=min(4, floor(n/8)) 字: k=0 只回 '(len=n)', 否則 '前k...後k(len=n)'; 36 字元之 session token 為前 4 後 4(與 w-web-sso 格式相同)
71
+ * - 其他型別只回 '(typeof)', 絕不輸出原值
72
+ *
73
+ * @param {*} token 輸入權杖
74
+ * @returns {String|Array} 回傳遮罩後字串, 輸入陣列時回傳陣列
75
+ */
76
+ function maskTok(token) {
77
+ if (isarr(token)) {
78
+ return token.map(maskTok)
79
+ }
80
+ if (token === undefined || token === null || token === '') {
81
+ return ''
82
+ }
83
+ if (!isestr(token)) {
84
+ return `(${typeof token})`
85
+ }
86
+ let n = token.length
87
+ let k = Math.min(4, Math.floor(n / 8))
88
+ if (k === 0) {
89
+ return `(len=${n})`
90
+ }
91
+ return `${safe(token.slice(0, k))}...${safe(token.slice(-k))}(len=${n})`
92
+ }
93
+
94
+
95
+ /**
96
+ * 查詢參數遮罩: 淺拷貝, 鍵名小寫為 token 者以 maskTok 處理, 其餘原樣(供印出 req.query)
97
+ * 非物件輸入依 maskTok 處理
98
+ *
99
+ * @param {*} q 輸入查詢參數物件(Hapi 之 req.query 為 null-prototype 物件)
100
+ * @returns {Object|String|Array} 回傳遮罩後之淺拷貝
101
+ */
102
+ function maskQuery(q) {
103
+ if (q === null || typeof q !== 'object' || isarr(q)) {
104
+ return maskTok(q)
105
+ }
106
+ let r = {}
107
+ for (let k of Object.keys(q)) {
108
+ r[k] = (k.toLowerCase() === 'token') ? maskTok(q[k]) : q[k]
109
+ }
110
+ return r
111
+ }
112
+
113
+
114
+ /**
115
+ * 網址遮罩: http(s) 只留 origin + pathname, 捨棄 query / fragment / userinfo(其內常含權杖, 如 ?token=)
116
+ * - 陣列逐元素處理; 空值回 ''; 其他非字串只回 '(typeof)'
117
+ * - 非 http(s) 之網址(如 data:、javascript:)路徑即全部內容, 只回 protocol(同 w-web-sso src/maskUrl.mjs)
118
+ * - 無法解析回 '(invalid-url)'
119
+ *
120
+ * @param {*} u 輸入網址
121
+ * @returns {String|Array} 回傳可寫入 log 之字串, 輸入陣列時回傳陣列
122
+ */
123
+ function maskUrl(u) {
124
+ if (isarr(u)) {
125
+ return u.map(maskUrl)
126
+ }
127
+ if (u === undefined || u === null || u === '') {
128
+ return ''
129
+ }
130
+ if (!isestr(u)) {
131
+ return `(${typeof u})`
132
+ }
133
+ let x = null
134
+ try {
135
+ x = new URL(u)
136
+ }
137
+ catch (err) {
138
+ return '(invalid-url)'
139
+ }
140
+ if (x.protocol !== 'http:' &amp;&amp; x.protocol !== 'https:') {
141
+ return x.protocol
142
+ }
143
+ return `${x.origin}${x.pathname}`
144
+ }
145
+
146
+
147
+ export {
148
+ maskTok,
149
+ maskQuery,
150
+ maskUrl
151
+ }
152
+ </code></pre>
153
+ </article>
154
+ </section>
155
+
156
+
157
+
158
+
159
+
160
+
161
+ </div>
162
+
163
+ <br class="clear">
164
+
165
+ <footer>
166
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
167
+ </footer>
168
+
169
+ <script>prettyPrint();</script>
170
+ <script src="scripts/polyfill.js"></script>
171
+ <script src="scripts/linenumber.js"></script>
172
+
173
+
174
+
175
+ </body>
176
+ </html>