1+ import { Buffer } from "node:buffer" ;
12import { createHash } from "node:crypto" ;
23import type { StreamFn } from "openclaw/plugin-sdk/agent-core" ;
34import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry" ;
@@ -15,21 +16,26 @@ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._~:/+@=-]+$/u;
1516const REQUEST_ID_UNSAFE_CHARACTER_PATTERN = / [ ^ A - Z a - z 0 - 9 . _ ~ : / + @ = - ] / gu;
1617const ATTRIBUTION_PRINTABLE_ASCII_PATTERN = / ^ [ \x20 - \x7E ] + $ / u;
1718const ATTRIBUTION_NON_PRINTABLE_ASCII_PATTERN = / [ ^ \x20 - \x7E ] / gu;
19+ const ATTRIBUTION_ENCODED_SUFFIX_PATTERN = / ~ [ a - f 0 - 9 ] { 16 } $ / u;
1820const REQUEST_ID_SUFFIX_PATTERN = / : m o d e l : \d + $ / u;
21+ const REQUEST_ID_ENCODED_SUFFIX_PATTERN = / ~ [ a - f 0 - 9 ] { 16 } (?: : m o d e l : \d + ) ? $ / u;
1922
2023type BoundedIdPolicy = {
24+ encodedSuffixPattern : RegExp ;
2125 maxLength : number ;
2226 safePattern : RegExp ;
2327 unsafeCharacterPattern : RegExp ;
2428 preservedSuffixPattern ?: RegExp ;
2529} ;
2630
2731const ATTRIBUTION_ID_POLICY : BoundedIdPolicy = {
32+ encodedSuffixPattern : ATTRIBUTION_ENCODED_SUFFIX_PATTERN ,
2833 maxLength : ATTRIBUTION_VALUE_MAX_LENGTH ,
2934 safePattern : ATTRIBUTION_PRINTABLE_ASCII_PATTERN ,
3035 unsafeCharacterPattern : ATTRIBUTION_NON_PRINTABLE_ASCII_PATTERN ,
3136} ;
3237const REQUEST_ID_POLICY : BoundedIdPolicy = {
38+ encodedSuffixPattern : REQUEST_ID_ENCODED_SUFFIX_PATTERN ,
3339 maxLength : REQUEST_ID_MAX_LENGTH ,
3440 safePattern : REQUEST_ID_PATTERN ,
3541 unsafeCharacterPattern : REQUEST_ID_UNSAFE_CHARACTER_PATTERN ,
@@ -59,12 +65,20 @@ function sanitizeBoundedId(value: string | undefined, policy: BoundedIdPolicy):
5965 if ( ! normalized ) {
6066 return undefined ;
6167 }
62- if ( normalized . length <= policy . maxLength && policy . safePattern . test ( normalized ) ) {
68+ if (
69+ normalized . length <= policy . maxLength &&
70+ policy . safePattern . test ( normalized ) &&
71+ ! policy . encodedSuffixPattern . test ( normalized )
72+ ) {
6373 return normalized ;
6474 }
65- // Fetch converts header values to ByteString. Keep automatic IDs ASCII,
66- // bounded, readable, and collision-resistant before transport construction.
67- const hash = createHash ( "sha256" ) . update ( normalized ) . digest ( "hex" ) . slice ( 0 , ID_HASH_LENGTH ) ;
75+ // Hash UTF-16 code units losslessly: UTF-8 string hashing replaces lone
76+ // surrogates with U+FFFD. The reserved encoded suffix keeps a rewritten ID
77+ // from aliasing an otherwise safe input that already looks encoded.
78+ const hash = createHash ( "sha256" )
79+ . update ( Buffer . from ( normalized , "utf16le" ) )
80+ . digest ( "hex" )
81+ . slice ( 0 , ID_HASH_LENGTH ) ;
6882 const preservedSuffix = policy . preservedSuffixPattern ?. exec ( normalized ) ?. [ 0 ] ?? "" ;
6983 const rawPrefix = preservedSuffix ? normalized . slice ( 0 , - preservedSuffix . length ) : normalized ;
7084 const safePrefix = rawPrefix . replace ( policy . unsafeCharacterPattern , "_" ) ;
0 commit comments